Restore a Local Backup v2

This commit is contained in:
Nicholas Tinsley
2024-04-18 14:44:49 -04:00
committed by Greyson Parrelli
parent 947ab7d48b
commit 62cf3feeaa
22 changed files with 1221 additions and 10 deletions

View File

@@ -844,6 +844,13 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".restore.RestoreActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.keyvalue.InternalValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
@@ -29,6 +30,7 @@ import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
@@ -53,6 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_RESTORE_BACKUP = 11;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -127,8 +130,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private void routeApplicationState(boolean locked) {
Intent intent = getIntentForState(getApplicationState(locked));
final int applicationState = getApplicationState(locked);
Intent intent = getIntentForState(applicationState);
if (intent != null) {
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
startActivity(intent);
finish();
}
@@ -148,6 +153,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_RESTORE_BACKUP: return getRestoreIntent();
default: return null;
}
}
@@ -161,6 +167,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.internalValues().enterRestoreV2Flow()) {
return STATE_RESTORE_BACKUP;
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
@@ -233,6 +241,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return getRoutedIntent(CreateSvrPinActivity.class, intent);
}
private Intent getRestoreIntent() {
Intent intent = RestoreActivity.getIntentForRestore(this);
return getRoutedIntent(intent, getIntent());
}
private Intent getCreateProfileNameIntent() {
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayOutputStream;
@@ -270,6 +271,13 @@ public class FullBackupImporter extends FullBackupBase {
return;
}
if (FeatureFlags.registrationV2()) {
if (SignalStore.account().getKeysToIncludeInBackup().contains(keyValue.key)) {
Log.i(TAG, "[regv2] skipping restore of " + keyValue.key);
return;
}
}
if (keyValue.blobValue != null) {
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
} else if (keyValue.booleanValue != null) {

View File

@@ -31,6 +31,7 @@ public final class InternalValues extends SignalStoreValues {
public static final String FORCE_WEBSOCKET_MODE = "internal.force_websocket_mode";
public static final String LAST_SCROLL_POSITION = "internal.last_scroll_position";
public static final String CONVERSATION_ITEM_V2_MEDIA = "internal.conversation_item_v2_media";
public static final String FORCE_ENTER_RESTORE_V2_FLOW = "internal.force_enter_restore_v2_flow";
InternalValues(KeyValueStore store) {
super(store);
@@ -210,4 +211,12 @@ public final class InternalValues extends SignalStoreValues {
public boolean useConversationItemV2Media() {
return FeatureFlags.internalUser() && getBoolean(CONVERSATION_ITEM_V2_MEDIA, false);
}
public void setForceEnterRestoreV2Flow(boolean enter) {
putBoolean(FORCE_ENTER_RESTORE_V2_FLOW, enter);
}
public boolean enterRestoreV2Flow() {
return FeatureFlags.registrationV2() && getBoolean(FORCE_ENTER_RESTORE_V2_FLOW, false);
}
}

View File

@@ -23,6 +23,7 @@ import androidx.appcompat.widget.Toolbar;
import com.google.android.material.button.MaterialButton;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.pin.PinOptOutDialog;
@@ -151,8 +152,11 @@ public abstract class BaseSvrPinFragment<ViewModel extends BaseSvrPinViewModel>
protected void closeNavGraphBranch() {
Intent activityIntent = requireActivity().getIntent();
if (activityIntent != null && activityIntent.hasExtra("next_intent")) {
startActivity(activityIntent.getParcelableExtra("next_intent"));
if (activityIntent != null && activityIntent.hasExtra(PassphraseRequiredActivity.NEXT_INTENT_EXTRA)) {
final Intent nextIntent = activityIntent.getParcelableExtra(PassphraseRequiredActivity.NEXT_INTENT_EXTRA);
if (nextIntent != null) {
startActivity(nextIntent);
}
}
requireActivity().finish();

View File

@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.jobs.RotateCertificateJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.pin.SvrRepository.onRegistrationComplete
import org.thoughtcrime.securesms.pin.SvrRepository
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -188,7 +188,7 @@ object RegistrationRepository {
TextSecurePreferences.setUnauthorizedReceived(context, false)
NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID)
onRegistrationComplete(response.masterKey, response.pin, hasPin, reglockEnabled)
SvrRepository.onRegistrationComplete(response.masterKey, response.pin, hasPin, reglockEnabled)
ApplicationDependencies.closeConnections()
ApplicationDependencies.getIncomingMessageObserver()
@@ -335,7 +335,11 @@ object RegistrationRepository {
val eventBus = EventBus.getDefault()
eventBus.register(subscriber)
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc).successOrThrow() // TODO: error handling
val sessionCreationResponse = accountManager.createRegistrationSession(fcmToken, mcc, mnc)
if (sessionCreationResponse !is NetworkResult.Success) {
return@withContext sessionCreationResponse
}
val receivedPush = subscriber.latch.await(PUSH_REQUEST_TIMEOUT, TimeUnit.MILLISECONDS)
eventBus.unregister(subscriber)
@@ -343,7 +347,7 @@ object RegistrationRepository {
val challenge = subscriber.challenge
if (challenge != null) {
Log.w(TAG, "Push challenge token received.")
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.body.id, challenge)
return@withContext accountManager.submitPushChallengeToken(sessionCreationResponse.result.body.id, challenge)
} else {
Log.w(TAG, "Push received but challenge token was null.")
}

View File

@@ -9,15 +9,15 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.registration.v2.ui.shared.RegistrationV2ViewModel
/**
* Activity to hold the entire registration process.
*/
class RegistrationV2Activity : AppCompatActivity() {
class RegistrationV2Activity : BaseActivity() {
private val TAG = Log.tag(RegistrationV2Activity::class.java)

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRegistrationEnterCodeV2Binding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
@@ -91,9 +92,12 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
Log.i(TAG, "Pin restore flow not required. Profile name: $isProfileNameEmpty | Profile avatar: $isAvatarEmpty | Needs PIN: $needsPin")
SignalStore.internalValues().setForceEnterRestoreV2Flow(true)
if (!needsProfile && !needsPin) {
sharedViewModel.completeRegistration()
}
sharedViewModel.setInProgress(false)
val startIntent = MainActivity.clearTop(activity).apply {
if (needsPin) {
@@ -105,8 +109,8 @@ class EnterCodeV2Fragment : LoggingFragment(R.layout.fragment_registration_enter
}
}
Log.d(TAG, "Launching ${startIntent.component}")
activity.startActivity(startIntent)
sharedViewModel.setInProgress(false)
activity.finish()
ActivityNavigator.applyPopAnimationsToPendingTransition(activity)
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import org.signal.core.util.getParcelableExtraCompat
import org.thoughtcrime.securesms.BaseActivity
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
/**
* Activity to hold the restore from backup flow.
*/
class RestoreActivity : BaseActivity() {
private val sharedViewModel: RestoreViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_restore)
intent.getParcelableExtraCompat(PassphraseRequiredActivity.NEXT_INTENT_EXTRA, Intent::class.java)?.let {
sharedViewModel.setNextIntent(it)
}
}
companion object {
@JvmStatic
fun getIntentForRestore(context: Context): Intent {
return Intent(context, RestoreActivity::class.java)
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore
import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.AppInitialization
import org.thoughtcrime.securesms.backup.BackupPassphrase
import org.thoughtcrime.securesms.backup.FullBackupImporter
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.service.LocalBackupListener
import org.thoughtcrime.securesms.util.BackupUtil
import java.io.IOException
/**
* Repository to handle restoring a backup of a user's message history.
*/
object RestoreRepository {
private val TAG = Log.tag(RestoreRepository.javaClass)
suspend fun getLocalBackupFromUri(context: Context, uri: Uri): BackupUtil.BackupInfo? = withContext(Dispatchers.IO) {
BackupUtil.getBackupInfoFromSingleUri(context, uri)
}
suspend fun restoreBackupAsynchronously(context: Context, backupFileUri: Uri, passphrase: String): BackupImportResult = withContext(Dispatchers.IO) {
// TODO [regv2]: migrate this to a service
try {
Log.i(TAG, "Starting backup restore.")
DataRestoreConstraint.isRestoringData = true
val database = SignalDatabase.backupDatabase
BackupPassphrase.set(context, passphrase)
if (!FullBackupImporter.validatePassphrase(context, backupFileUri, passphrase)) {
// TODO [regv2]: implement a specific, user-visible error for wrong passphrase.
return@withContext BackupImportResult.FAILURE_UNKNOWN
}
FullBackupImporter.importFile(
context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
database,
backupFileUri,
passphrase
)
SignalDatabase.runPostBackupRestoreTasks(database)
NotificationChannels.getInstance().restoreContactNotificationChannels()
if (BackupUtil.canUserAccessBackupDirectory(context)) {
LocalBackupListener.setNextBackupTimeToIntervalFromNow(context)
SignalStore.settings().isBackupEnabled = true
LocalBackupListener.schedule(context)
}
AppInitialization.onPostBackupRestore(context)
Log.i(TAG, "Backup restore complete.")
return@withContext BackupImportResult.SUCCESS
} catch (e: FullBackupImporter.DatabaseDowngradeException) {
Log.w(TAG, "Failed due to the backup being from a newer version of Signal.", e)
return@withContext BackupImportResult.FAILURE_VERSION_DOWNGRADE
} catch (e: FullBackupImporter.ForeignKeyViolationException) {
Log.w(TAG, "Failed due to foreign key constraint violations.", e)
return@withContext BackupImportResult.FAILURE_FOREIGN_KEY
} catch (e: IOException) {
Log.w(TAG, e)
return@withContext BackupImportResult.FAILURE_UNKNOWN
} finally {
DataRestoreConstraint.isRestoringData = false
}
}
enum class BackupImportResult {
SUCCESS,
FAILURE_VERSION_DOWNGRADE,
FAILURE_FOREIGN_KEY,
FAILURE_UNKNOWN
}
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore
import android.content.Intent
import android.net.Uri
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
/**
* Shared state holder for the restore flow.
*/
data class RestoreState(val restorationType: BackupRestorationType = BackupRestorationType.LOCAL_BACKUP, val backupFile: Uri? = null, val nextIntent: Intent? = null)

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
/**
* Shared view model for the restore flow.
*/
class RestoreViewModel : ViewModel() {
private val store = MutableStateFlow(RestoreState())
val uiState = store.asLiveData()
fun setNextIntent(nextIntent: Intent) {
store.update {
it.copy(nextIntent = nextIntent)
}
}
fun onTransferFromAndroidDeviceSelected() {
store.update {
it.copy(restorationType = BackupRestorationType.DEVICE_TRANSFER)
}
}
fun onRestoreFromLocalBackupSelected() {
store.update {
it.copy(restorationType = BackupRestorationType.LOCAL_BACKUP)
}
}
fun onRestoreFromRemoteBackupSelected() {
store.update {
it.copy(restorationType = BackupRestorationType.REMOTE_BACKUP)
}
}
fun getBackupRestorationType(): BackupRestorationType {
return store.value.restorationType
}
fun setBackupFileUri(backupFileUri: Uri) {
store.update {
it.copy(backupFile = backupFileUri)
}
}
fun getBackupFileUri(): Uri? = store.value.backupFile
fun getNextIntent(): Intent? = store.value.nextIntent
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore.choosebackup
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.text.HtmlCompat
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentChooseBackupV2Binding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* This fragment presents a button to the user to browse their local file system for a legacy backup file.
*/
class ChooseBackupV2Fragment : LoggingFragment(R.layout.fragment_choose_backup_v2) {
private val sharedViewModel by activityViewModels<RestoreViewModel>()
private val binding: FragmentChooseBackupV2Binding by ViewBinderDelegate(FragmentChooseBackupV2Binding::bind)
private val pickMedia = registerForActivityResult(BackupFileContract()) {
if (it != null) {
onUserChoseBackupFile(it)
} else {
Log.i(TAG, "Null URI returned for backup file selection.")
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.chooseBackupFragmentTitle)
binding.chooseBackupFragmentButton.setOnClickListener { onChooseBackupSelected() }
binding.chooseBackupFragmentLearnMore.text = HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0)
binding.chooseBackupFragmentLearnMore.movementMethod = LinkMovementMethod.getInstance()
}
private fun onChooseBackupSelected() {
pickMedia.launch("application/octet-stream")
}
private fun onUserChoseBackupFile(backupFileUri: Uri) {
sharedViewModel.setBackupFileUri(backupFileUri)
NavHostFragment.findNavController(this).safeNavigate(ChooseBackupV2FragmentDirections.actionChooseLocalBackupFragmentToRestoreLocalBackupFragment())
}
private class BackupFileContract : ActivityResultContracts.GetContent() {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input).apply {
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
if (Build.VERSION.SDK_INT >= 26) {
putExtra(DocumentsContract.EXTRA_INITIAL_URI, SignalStore.settings().latestSignalBackupDirectory)
}
}
}
}
companion object {
private val TAG = Log.tag(ChooseBackupV2Fragment::class.java)
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore.restorelocalbackup
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.BackupEvent
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentRestoreLocalBackupV2Binding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment.PassphraseAsYouTypeFormatter
import org.thoughtcrime.securesms.restore.RestoreRepository
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.ViewModelFactory
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.util.Locale
/**
* This fragment is used to monitor and manage an in-progress backup restore.
*/
class RestoreLocalBackupFragment : LoggingFragment(R.layout.fragment_restore_local_backup_v2) {
private val navigationViewModel: RestoreViewModel by activityViewModels()
private val restoreLocalBackupViewModel: RestoreLocalBackupViewModel by viewModels(
factoryProducer = ViewModelFactory.factoryProducer {
val fileBackupUri = navigationViewModel.getBackupFileUri()!!
RestoreLocalBackupViewModel(fileBackupUri)
}
)
private val binding: FragmentRestoreLocalBackupV2Binding by ViewBinderDelegate(FragmentRestoreLocalBackupV2Binding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setDebugLogSubmitMultiTapView(binding.verifyHeader)
Log.i(TAG, "Backup restore.")
restoreLocalBackupViewModel.uiState.observe(viewLifecycleOwner) { fragmentState ->
fragmentState.backupInfo?.let {
presentBackupFileInfo(backupSize = it.size, backupTimestamp = it.timestamp)
if (fragmentState.backupPassphrase.isEmpty()) {
presentBackupPassPhrasePromptDialog()
}
}
if (fragmentState.restoreInProgress) {
presentRestoreProgress(fragmentState.backupProgressCount)
} else {
presentProgressEnded()
}
if (fragmentState.backupRestoreComplete) {
val importResult = fragmentState.backupImportResult
if (importResult == null) {
onBackupCompletedSuccessfully()
} else {
handleBackupImportResult(importResult)
}
}
}
restoreLocalBackupViewModel.startRestore(requireContext())
}
private fun onBackupCompletedSuccessfully() {
Log.d(TAG, "onBackupCompletedSuccessfully()")
SignalStore.internalValues().setForceEnterRestoreV2Flow(false)
val activity = requireActivity()
navigationViewModel.getNextIntent()?.let {
Log.d(TAG, "Launching ${it.component}")
activity.startActivity(it)
}
activity.finish()
}
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(event: BackupEvent) {
restoreLocalBackupViewModel.onBackupProgressUpdate(event)
}
private fun handleBackupImportResult(importResult: RestoreRepository.BackupImportResult) {
when (importResult) {
RestoreRepository.BackupImportResult.FAILURE_VERSION_DOWNGRADE -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_downgrade, Toast.LENGTH_LONG).show()
RestoreRepository.BackupImportResult.FAILURE_FOREIGN_KEY -> Toast.makeText(requireContext(), R.string.RegistrationActivity_backup_failure_foreign_key, Toast.LENGTH_LONG).show()
RestoreRepository.BackupImportResult.FAILURE_UNKNOWN -> Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_backup_passphrase, Toast.LENGTH_LONG).show()
RestoreRepository.BackupImportResult.SUCCESS -> Log.w(TAG, "Successful backup import should not be handled here.", IllegalStateException())
}
}
private fun presentProgressEnded() {
binding.restoreButton.cancelSpinning()
binding.cancelLocalRestoreButton.visible = true
binding.backupProgressText.text = ""
}
private fun presentRestoreProgress(backupProgressCount: Long) {
binding.restoreButton.setSpinning()
binding.cancelLocalRestoreButton.visibility = View.INVISIBLE
if (backupProgressCount > 0L) {
binding.backupProgressText.text = getString(R.string.RegistrationActivity_d_messages_so_far, backupProgressCount)
} else {
binding.backupProgressText.setText(R.string.RegistrationActivity_checking)
}
}
private fun presentBackupPassPhrasePromptDialog() {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.enter_backup_passphrase_dialog, null)
val prompt = view.findViewById<EditText>(R.id.restore_passphrase_input)
prompt.addTextChangedListener(PassphraseAsYouTypeFormatter())
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationActivity_enter_backup_passphrase)
.setView(view)
.setPositiveButton(R.string.RegistrationActivity_restore) { _, _ ->
ViewUtil.hideKeyboard(requireContext(), prompt)
val passphrase = prompt.getText().toString()
restoreLocalBackupViewModel.confirmPassphrase(requireContext(), passphrase)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
Log.i(TAG, "Prompt for backup passphrase shown to user.")
}
private fun presentBackupFileInfo(backupSize: Long, backupTimestamp: Long) {
if (backupSize > 0) {
binding.backupSizeText.text = getString(R.string.RegistrationActivity_backup_size_s, Util.getPrettyFileSize(backupSize))
}
if (backupTimestamp > 0) {
binding.backupCreatedText.text = getString(R.string.RegistrationActivity_backup_timestamp_s, DateUtils.getExtendedRelativeTimeSpanString(requireContext(), Locale.getDefault(), backupTimestamp))
}
}
companion object {
private val TAG = Log.tag(RestoreLocalBackupFragment::class.java)
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore.restorelocalbackup
import android.net.Uri
import org.thoughtcrime.securesms.restore.RestoreRepository
import org.thoughtcrime.securesms.util.BackupUtil.BackupInfo
/**
* State holder for a backup restore.
*/
data class RestoreLocalBackupState(
val uri: Uri,
val backupInfo: BackupInfo? = null,
val backupPassphrase: String = "",
val restoreInProgress: Boolean = false,
val backupVerifyingInProgress: Boolean = false,
val backupProgressCount: Long = -1,
val backupEstimatedTotalCount: Long = -1,
val backupRestoreComplete: Boolean = false,
val backupImportResult: RestoreRepository.BackupImportResult? = null,
val abort: Boolean = false
)

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore.restorelocalbackup
import android.content.Context
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.BackupEvent
import org.thoughtcrime.securesms.restore.RestoreRepository
/**
* ViewModel for [RestoreLocalBackupFragment]
*/
class RestoreLocalBackupViewModel(fileBackupUri: Uri) : ViewModel() {
private val store = MutableStateFlow(RestoreLocalBackupState(fileBackupUri))
val uiState = store.asLiveData()
fun startRestore(context: Context) {
val backupFileUri = store.value.uri
viewModelScope.launch {
val backupInfo = RestoreRepository.getLocalBackupFromUri(context, backupFileUri)
if (backupInfo == null) {
abort()
return@launch
}
store.update {
it.copy(
backupInfo = backupInfo
)
}
}
}
private fun abort() {
store.update {
it.copy(abort = true)
}
}
fun confirmPassphrase(context: Context, passphrase: String) {
store.update {
it.copy(
backupPassphrase = passphrase,
restoreInProgress = true
)
}
val backupFileUri = store.value.backupInfo?.uri
val backupPassphrase = store.value.backupPassphrase
if (backupFileUri == null) {
Log.w(TAG, "Could not begin backup import because backup file URI was null!")
abort()
return
}
if (backupPassphrase.isEmpty()) {
Log.w(TAG, "Could not begin backup import because backup passphrase was empty!")
abort()
return
}
viewModelScope.launch {
val importResult = RestoreRepository.restoreBackupAsynchronously(context, backupFileUri, backupPassphrase)
store.update {
it.copy(
backupImportResult = if (importResult == RestoreRepository.BackupImportResult.SUCCESS) null else importResult,
restoreInProgress = false,
backupRestoreComplete = true,
backupEstimatedTotalCount = -1L,
backupProgressCount = -1L,
backupVerifyingInProgress = false
)
}
}
}
fun onBackupProgressUpdate(event: BackupEvent) {
store.update {
it.copy(
backupProgressCount = event.count,
backupEstimatedTotalCount = event.estimatedTotalCount,
backupVerifyingInProgress = event.type == BackupEvent.Type.PROGRESS_VERIFYING,
backupRestoreComplete = event.type == BackupEvent.Type.FINISHED,
restoreInProgress = event.type != BackupEvent.Type.FINISHED
)
}
}
companion object {
private val TAG = Log.tag(RestoreLocalBackupViewModel::class.java)
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.restore.transferorrestore
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.NavHostFragment
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.databinding.FragmentTransferRestoreV2Binding
import org.thoughtcrime.securesms.devicetransfer.newdevice.BackupRestorationType
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.restore.RestoreViewModel
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
/**
* This presents a list of options for the user to restore (or skip) a backup.
*/
class TransferOrRestoreV2Fragment : LoggingFragment(R.layout.fragment_transfer_restore_v2) {
private val sharedViewModel by activityViewModels<RestoreViewModel>()
private val binding: FragmentTransferRestoreV2Binding by ViewBinderDelegate(FragmentTransferRestoreV2Binding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.transferOrRestoreTitle)
binding.transferOrRestoreFragmentTransfer.setOnClickListener { sharedViewModel.onTransferFromAndroidDeviceSelected() }
binding.transferOrRestoreFragmentRestore.setOnClickListener { sharedViewModel.onRestoreFromLocalBackupSelected() }
binding.transferOrRestoreFragmentRestoreRemote.setOnClickListener { sharedViewModel.onRestoreFromRemoteBackupSelected() }
binding.transferOrRestoreFragmentNext.setOnClickListener { launchSelection(sharedViewModel.getBackupRestorationType()) }
binding.transferOrRestoreFragmentMoreOptions.setOnClickListener {
Log.w(TAG, "Not yet implemented!", NotImplementedError()) // TODO [regv2]
}
binding.transferOrRestoreFragmentRestoreRemoteCard.visible = FeatureFlags.messageBackups()
binding.transferOrRestoreFragmentMoreOptions.visible = FeatureFlags.messageBackups()
val description = getString(R.string.TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device)
val toBold = getString(R.string.TransferOrRestoreFragment__you_need_access_to_your_old_device)
binding.transferOrRestoreFragmentTransferDescription.text = SpanUtil.boldSubstring(description, toBold)
sharedViewModel.uiState.observe(viewLifecycleOwner) { state ->
updateSelection(state.restorationType)
}
// TODO [regv2]: port backup file detection to here
}
private fun updateSelection(restorationType: BackupRestorationType) {
binding.transferOrRestoreFragmentTransferCard.isSelected = restorationType == BackupRestorationType.DEVICE_TRANSFER
binding.transferOrRestoreFragmentRestoreCard.isSelected = restorationType == BackupRestorationType.LOCAL_BACKUP
binding.transferOrRestoreFragmentRestoreRemoteCard.isSelected = restorationType == BackupRestorationType.REMOTE_BACKUP
}
private fun launchSelection(restorationType: BackupRestorationType) {
when (restorationType) {
BackupRestorationType.DEVICE_TRANSFER -> {
// TODO [regv2]
Log.w(TAG, "Not yet implemented!", NotImplementedError())
Toast.makeText(requireContext(), "Not yet implemented!", Toast.LENGTH_LONG).show()
}
BackupRestorationType.LOCAL_BACKUP -> {
NavHostFragment.findNavController(this).safeNavigate(TransferOrRestoreV2FragmentDirections.actionTransferOrRestoreToRestore())
}
BackupRestorationType.REMOTE_BACKUP -> {
// TODO [regv2]
Log.w(TAG, "Not yet implemented!", NotImplementedError())
Toast.makeText(requireContext(), "Not yet implemented!", Toast.LENGTH_LONG).show()
}
else -> {
throw IllegalArgumentException()
}
}
}
companion object {
private val TAG = Log.tag(TransferOrRestoreV2Fragment::class.java)
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".restore.RestoreActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/restore" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/choose_backup_fragment_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="64dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/ChooseBackupFragment__restore_from_backup"
android:textAppearance="@style/Signal.Text.HeadlineMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/choose_backup_fragment_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/ChooseBackupFragment__restore_your_messages_and_media"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_title" />
<TextView
android:id="@+id/choose_backup_fragment_learn_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/ChooseBackupFragment__learn_more"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_message" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/choose_backup_fragment_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/circle_tintable"
android:contentDescription="@string/ChooseBackupFragment__icon_content_description"
android:padding="30dp"
app:backgroundTint="@color/signal_colorSurfaceVariant"
app:layout_constraintBottom_toTopOf="@+id/choose_backup_fragment_button"
app:layout_constraintDimensionRatio="H,1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_max="120dp"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/choose_backup_fragment_learn_more"
app:layout_constraintWidth_max="120dp"
app:srcCompat="@drawable/ic_backup_outline_60"
app:tint="@color/signal_colorPrimary" />
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
android:id="@+id/choose_backup_fragment_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="32dp"
app:circularProgressMaterialButton__label="@string/ChooseBackupFragment__choose_backup"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:materialThemeOverlay="@style/ThemeOverlay.Signal.CircularProgressIndicator.Primary" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".registration.fragments.RestoreBackupFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_local_restore_button"
style="@style/Signal.Widget.Button.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:paddingStart="30dp"
android:paddingTop="10dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
android:text="@string/RegistrationActivity_cancel"
android:textColor="@color/signal_text_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restore_button" />
<TextView
android:id="@+id/verify_subheader"
style="@style/Signal.Text.Body.Registration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_restore_your_messages_and_media_from_a_local_backup"
app:layout_constraintTop_toBottomOf="@+id/verify_header"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="@+id/backup_created_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintStart_toStartOf="@+id/verify_subheader"
app:layout_constraintTop_toBottomOf="@+id/verify_subheader"
tools:text="Backup created: 1 min ago" />
<TextView
android:id="@+id/backup_size_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="@+id/backup_created_text"
app:layout_constraintTop_toBottomOf="@+id/backup_created_text"
tools:text="Backup size: 899 KB" />
<TextView
android:id="@+id/backup_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backup_size_text"
tools:text="100 messages so far..." />
<TextView
android:id="@+id/verify_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="32dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="32dp"
android:gravity="center"
android:text="@string/RegistrationActivity_restore_from_backup"
android:textAppearance="@style/Signal.Text.HeadlineMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
android:id="@+id/restore_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:circularProgressMaterialButton__label="@string/registration_activity__restore_backup"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/backup_progress_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@@ -0,0 +1,244 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="32dp"
android:paddingTop="@dimen/transfer_top_padding"
android:paddingEnd="32dp"
android:paddingBottom="24dp">
<TextView
android:id="@+id/transfer_or_restore_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
android:text="@string/TransferOrRestoreFragment__transfer_or_restore_account"
android:textAppearance="@style/Signal.Text.HeadlineMedium" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/transfer_item_spacing"
android:gravity="center"
android:text="@string/TransferOrRestoreFragment__if_you_have_previously_registered_a_signal_account"
android:textAppearance="@style/Signal.Text.BodyLarge"
android:textColor="@color/signal_colorOnSurfaceVariant" />
<org.thoughtcrime.securesms.components.ClippedCardView
android:id="@+id/transfer_or_restore_fragment_transfer_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/signal_colorSurface2"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeColor="@color/transfer_option_stroke_color_selector"
app:strokeWidth="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/transfer_or_restore_fragment_transfer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/transfer_or_restore_fragment_transfer_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_transfer_phone_48"
app:tint="@color/signal_colorPrimary" />
<TextView
android:id="@+id/transfer_or_restore_fragment_transfer_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="16dp"
android:text="@string/TransferOrRestoreFragment__transfer_from_android_device"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_transfer_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_transfer_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/transfer_or_restore_fragment_transfer_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_transfer_header"
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_transfer_header"
tools:text="@string/TransferOrRestoreFragment__transfer_your_account_and_messages_from_your_old_android_device" />
</androidx.constraintlayout.widget.ConstraintLayout>
</org.thoughtcrime.securesms.components.ClippedCardView>
<org.thoughtcrime.securesms.components.ClippedCardView
android:id="@+id/transfer_or_restore_fragment_restore_remote_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:cardBackgroundColor="@color/signal_colorSurface2"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeColor="@color/transfer_option_stroke_color_selector"
app:strokeWidth="2dp"
tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/transfer_or_restore_fragment_restore_remote"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/transfer_or_restore_fragment_restore_remote_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/symbol_backup_light"
app:tint="@color/signal_colorPrimary" />
<TextView
android:id="@+id/transfer_or_restore_fragment_restore_remote_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="16dp"
android:text="@string/TransferOrRestoreFragment__restore_from_backup"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_remote_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_restore_remote_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/transfer_or_restore_fragment_restore_remote_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_restore_remote_header"
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_restore_remote_header" />
</androidx.constraintlayout.widget.ConstraintLayout>
</org.thoughtcrime.securesms.components.ClippedCardView>
<org.thoughtcrime.securesms.components.ClippedCardView
android:id="@+id/transfer_or_restore_fragment_restore_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="@color/signal_colorSurface2"
app:cardCornerRadius="12dp"
app:cardElevation="0dp"
app:strokeColor="@color/transfer_option_stroke_color_selector"
app:strokeWidth="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/transfer_or_restore_fragment_restore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="16dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/transfer_or_restore_fragment_restore_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginStart="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/symbol_backup_light"
app:tint="@color/signal_colorPrimary" />
<TextView
android:id="@+id/transfer_or_restore_fragment_restore_header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="18dp"
android:layout_marginEnd="16dp"
android:text="@string/TransferOrRestoreFragment__restore_from_backup"
android:textAppearance="@style/Signal.Text.Body"
app:layout_constraintBottom_toTopOf="@+id/transfer_or_restore_fragment_restore_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@+id/transfer_or_restore_fragment_restore_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/transfer_or_restore_fragment_restore_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="@string/TransferOrRestoreFragment__restore_your_messages_from_a_local_backup"
android:textColor="@color/signal_colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="@+id/transfer_or_restore_fragment_restore_header"
app:layout_constraintTop_toBottomOf="@+id/transfer_or_restore_fragment_restore_header" />
</androidx.constraintlayout.widget.ConstraintLayout>
</org.thoughtcrime.securesms.components.ClippedCardView>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/transfer_or_restore_fragment_more_options"
style="@style/Widget.Signal.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/TransferOrRestoreFragment__more_options"
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/transfer_or_restore_fragment_next"
style="@style/Signal.Widget.Button.Large.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/RegistrationActivity_next" />
</FrameLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/restore"
app:startDestination="@id/transfer_or_restore_v2_fragment">
<fragment
android:id="@+id/transfer_or_restore_v2_fragment"
android:name="org.thoughtcrime.securesms.restore.transferorrestore.TransferOrRestoreV2Fragment"
android:label="transfer_or_restore"
tools:layout="@layout/fragment_transfer_restore_v2">
<action
android:id="@+id/action_transfer_or_restore_to_restore"
app:destination="@id/choose_local_backup_fragment" />
</fragment>
<fragment
android:id="@+id/choose_local_backup_fragment"
android:name="org.thoughtcrime.securesms.restore.choosebackup.ChooseBackupV2Fragment"
android:label="choose_local_backup"
tools:layout="@layout/fragment_choose_backup_v2">
<action
android:id="@+id/action_choose_local_backup_fragment_to_restore_local_backup_fragment"
app:destination="@id/restore_local_backup_fragment" />
</fragment>
<fragment
android:id="@+id/restore_local_backup_fragment"
android:name="org.thoughtcrime.securesms.restore.restorelocalbackup.RestoreLocalBackupFragment"
android:label="restore_local_backup"
tools:layout="@layout/fragment_restore_local_backup_v2">
</fragment>
</navigation>