diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupDialog.kt index d72d8596a9..38f9c7f4f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupDialog.kt @@ -13,13 +13,15 @@ import org.thoughtcrime.securesms.R enum class RestoreLocalBackupDialog { FAILED_TO_LOAD_ARCHIVE, - SKIP_RESTORE_WARNING + SKIP_RESTORE_WARNING, + CONFIRM_DIFFERENT_ACCOUNT } @Composable fun RestoreLocalBackupDialogDisplay( dialog: RestoreLocalBackupDialog?, onDialogConfirmed: (RestoreLocalBackupDialog) -> Unit, + onDialogDenied: (RestoreLocalBackupDialog) -> Unit, onDismiss: () -> Unit ) { when (dialog) { @@ -33,9 +35,9 @@ fun RestoreLocalBackupDialogDisplay( RestoreLocalBackupDialog.SKIP_RESTORE_WARNING -> { Dialogs.SimpleAlertDialog( - title = "Skip restore?", - body = "If you skip restore now you will not be able to restore later. If you re-enable backups after skipping restore, your current backup will be replaced with your new messaging history.", - confirm = "Skip restore", + title = stringResource(R.string.RestoreLocalBackupDialog__skip_restore), + body = stringResource(R.string.RestoreLocalBackupDialog__skip_restore_body), + confirm = stringResource(R.string.RestoreLocalBackupDialog__skip_restore_confirm), confirmColor = MaterialTheme.colorScheme.error, onConfirm = { onDialogConfirmed(RestoreLocalBackupDialog.SKIP_RESTORE_WARNING) @@ -44,6 +46,21 @@ fun RestoreLocalBackupDialogDisplay( ) } + RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.RestoreLocalBackupDialog__restore_to_new_account), + body = stringResource(R.string.RestoreLocalBackupDialog__restore_to_new_account_body), + confirm = stringResource(R.string.RestoreLocalBackupDialog__restore), + dismiss = stringResource(android.R.string.cancel), + onConfirm = { + onDialogConfirmed(RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT) + }, + onDeny = { + onDialogDenied(RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT) + } + ) + } + null -> return } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt index 6925c65fea..e762e2f2d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt @@ -81,14 +81,7 @@ class RestoreLocalBackupFragment : ComposeFragment() { .collect { sharedViewModel.registerAccountErrorShown() if (it is RegisterAccountResult.IncorrectRecoveryPassword) { - SignalStore.account.resetAccountEntropyPool() - SignalStore.account.resetAciAndPniIdentityKeysAfterFailedRestore() - sharedViewModel.clearRecoveryPassword() - enterBackupKeyViewModel.cancelRegistering() - sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = false, fromLocalV2 = true) - findNavController().safeNavigate( - RestoreLocalBackupFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION) - ) + restoreLocalBackupViewModel.displayDifferentAccountWarning() } else { enterBackupKeyViewModel.handleRegistrationFailure(it) } @@ -141,6 +134,29 @@ class RestoreLocalBackupFragment : ComposeFragment() { findNavController().safeNavigate(RestoreLocalBackupFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION)) } + override fun confirmRestoreWithDifferentAccount() { + SignalStore.account.resetAccountEntropyPool() + SignalStore.account.resetAciAndPniIdentityKeysAfterFailedRestore() + sharedViewModel.clearRecoveryPassword() + enterBackupKeyViewModel.cancelRegistering() + sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = false, fromLocalV2 = true) + findNavController().safeNavigate( + RestoreLocalBackupFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.RESTART_AFTER_COLLECTION) + ) + } + + override fun denyRestoreWithDifferentAccount() { + SignalStore.account.resetAccountEntropyPool() + SignalStore.account.resetAciAndPniIdentityKeysAfterFailedRestore() + SignalStore.backup.localRestoreAccountEntropyPool = null + sharedViewModel.clearRecoveryPassword() + enterBackupKeyViewModel.cancelRegistering() + sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = false, fromLocalV2 = true) + findNavController().safeNavigate( + RestoreLocalBackupFragmentDirections.goToEnterPhoneNumber(EnterPhoneNumberMode.COLLECT_FOR_LOCAL_V2_SIGNAL_BACKUPS_RESTORE) + ) + } + override fun routeToLegacyBackupRestoration(uri: Uri) { sharedViewModel.intendToRestore(hasOldDevice = false, fromRemote = false, fromLocalV2 = false) localBackupRestore.launch(RestoreActivity.getLocalRestoreIntent(requireContext(), uri)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt index 49fb885408..1e176e79d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt @@ -158,11 +158,23 @@ fun RestoreLocalBackupNavDisplay( } ) - RestoreLocalBackupDialogDisplay(state.dialog, { - if (it == RestoreLocalBackupDialog.SKIP_RESTORE_WARNING) { - callback.skipRestore() - } - }, callback::clearDialog) + RestoreLocalBackupDialogDisplay( + dialog = state.dialog, + onDialogConfirmed = { + when (it) { + RestoreLocalBackupDialog.SKIP_RESTORE_WARNING -> callback.skipRestore() + RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> callback.confirmRestoreWithDifferentAccount() + else -> Unit + } + }, + onDialogDenied = { + when (it) { + RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> callback.denyRestoreWithDifferentAccount() + else -> Unit + } + }, + onDismiss = callback::clearDialog + ) } data class RestoreLocalBackupState( @@ -177,6 +189,8 @@ interface RestoreLocalBackupCallback { fun displaySkipRestoreWarning() fun clearDialog() fun skipRestore() + fun confirmRestoreWithDifferentAccount() + fun denyRestoreWithDifferentAccount() fun submitBackupKey() fun routeToLegacyBackupRestoration(uri: Uri) fun onBackupKeyChanged(key: String) @@ -189,6 +203,8 @@ interface RestoreLocalBackupCallback { override fun displaySkipRestoreWarning() = Unit override fun clearDialog() = Unit override fun skipRestore() = Unit + override fun confirmRestoreWithDifferentAccount() = Unit + override fun denyRestoreWithDifferentAccount() = Unit override fun submitBackupKey() = Unit override fun routeToLegacyBackupRestoration(uri: Uri) = Unit override fun onBackupKeyChanged(key: String) = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt index 6b0d357767..f1b2d86bad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt @@ -10,13 +10,17 @@ import android.net.Uri import androidx.lifecycle.ViewModel import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.withContext +import org.signal.core.models.AccountEntropyPool import org.signal.core.util.bytes import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem +import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.DateUtils @@ -79,6 +83,31 @@ class RestoreLocalBackupViewModel : ViewModel() { internalState.update { it.copy(dialog = RestoreLocalBackupDialog.SKIP_RESTORE_WARNING) } } + fun displayDifferentAccountWarning() { + internalState.update { it.copy(dialog = RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT) } + } + + /** Returns true if the backup at [timestamp] was created by the currently registered account, false if it belongs to a different account. */ + suspend fun backupBelongsToCurrentAccount(context: Context, backupKey: String, timestamp: Long): Boolean { + return withContext(Dispatchers.IO) { + val aep = requireNotNull(AccountEntropyPool.parseOrNull(backupKey)) { "Backup key must be valid at submission time" } + val messageBackupKey = aep.deriveMessageBackupKey() + val dirUri = requireNotNull(SignalStore.backup.newLocalBackupsDirectory) { "Backup directory must be set" } + val archiveFileSystem = requireNotNull(ArchiveFileSystem.fromUri(context, Uri.parse(dirUri))) { "Backup directory must be accessible" } + val snapshot = requireNotNull(archiveFileSystem.listSnapshots().firstOrNull { it.timestamp == timestamp }) { "Selected snapshot must still exist" } + val snapshotFs = SnapshotFileSystem(context, snapshot.file) + val actualBackupId = LocalArchiver.getBackupId(snapshotFs, messageBackupKey) + if (actualBackupId == null) { + Log.w(TAG, "backupBelongsToCurrentAccount: getBackupId returned null, treating as current account") + return@withContext true + } + val expectedBackupId = messageBackupKey.deriveBackupId(SignalStore.account.requireAci()) + val matches = actualBackupId.value.contentEquals(expectedBackupId.value) + Log.d(TAG, "backupBelongsToCurrentAccount: matches=$matches") + matches + } + } + fun clearDialog() { internalState.update { it.copy(dialog = null) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt index a57b9acb8c..36d3ba5d27 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt @@ -99,14 +99,37 @@ class PostRegistrationRestoreLocalBackupFragment : ComposeFragment() { } } + override fun confirmRestoreWithDifferentAccount() { + launchRestore() + } + + override fun denyRestoreWithDifferentAccount() { + restoreLocalBackupViewModel.clearDialog() + } + override fun submitBackupKey() { AccountEntropyPool.parseOrNull(enterBackupKeyViewModel.backupKey) ?: return - - SignalStore.backup.localRestoreAccountEntropyPool = enterBackupKeyViewModel.backupKey - val selectedTimestamp = restoreLocalBackupViewModel.state.value.selectedBackup?.timestamp ?: -1L - SignalStore.backup.newLocalBackupsSelectedSnapshotTimestamp = selectedTimestamp + viewLifecycleOwner.lifecycleScope.launch { + val belongsToCurrentAccount = restoreLocalBackupViewModel.backupBelongsToCurrentAccount( + context = requireContext(), + backupKey = enterBackupKeyViewModel.backupKey, + timestamp = selectedTimestamp + ) + + if (belongsToCurrentAccount) { + launchRestore() + } else { + restoreLocalBackupViewModel.displayDifferentAccountWarning() + } + } + } + + private fun launchRestore() { + val selectedTimestamp = restoreLocalBackupViewModel.state.value.selectedBackup?.timestamp ?: -1L + SignalStore.backup.localRestoreAccountEntropyPool = enterBackupKeyViewModel.backupKey + SignalStore.backup.newLocalBackupsSelectedSnapshotTimestamp = selectedTimestamp startActivity(RestoreLocalBackupActivity.getIntent(requireContext())) requireActivity().supportFinishAfterTransition() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31cc99854e..b18a99ad0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9193,6 +9193,18 @@ Failed to load archive. Please select a different directory. OK + + Skip restore? + + If you skip restore now you will not be able to restore later. If you re-enable backups after skipping restore, your current backup will be replaced with your new messaging history. + + Skip restore + + Restore to new account? + + The backup you are restoring was created using a different Signal account. You can restore this backup to a new account, but you will no longer be a member of any groups that were restored. + + Restore Backup restore failed