diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt index a181a8f229..dd5ce14610 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayFragment.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import org.signal.core.ui.compose.Dialogs import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordMode @@ -42,6 +43,7 @@ class BackupKeyDisplayFragment : ComposeFragment() { } private val viewModel: BackupKeyDisplayViewModel by viewModel { BackupKeyDisplayViewModel() } + private val args: BackupKeyDisplayFragmentArgs by navArgs() @Composable override fun FragmentContent() { @@ -54,6 +56,12 @@ class BackupKeyDisplayFragment : ComposeFragment() { navController.enableOnBackPressed(true) } + LaunchedEffect(args.startWithKeyRotation, state.rotationState) { + if (args.startWithKeyRotation && state.rotationState == BackupKeyRotationState.NOT_STARTED) { + viewModel.rotateBackupKey() + } + } + LaunchedEffect(state.rotationState) { if (state.rotationState == BackupKeyRotationState.FINISHED) { setFragmentResult(AEP_ROTATION_KEY, bundleOf(AEP_ROTATION_KEY to true)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseSvrPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseSvrPinFragment.java index f36b356d8d..33dce70c78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseSvrPinFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseSvrPinFragment.java @@ -200,7 +200,7 @@ public abstract class BaseSvrPinFragment } private void onPinSkipped() { - PinOptOutDialog.show(requireContext(), () -> { + PinOptOutDialog.show(requireContext(), false, () -> { RegistrationUtil.maybeMarkRegistrationComplete(); closeNavGraphBranch(); }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/SvrSplashFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/SvrSplashFragment.java index 87a3038518..d48044b75f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/SvrSplashFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/SvrSplashFragment.java @@ -125,6 +125,6 @@ public final class SvrSplashFragment extends Fragment { } private void onPinSkipped() { - PinOptOutDialog.show(requireContext(), () -> requireActivity().finish()); + PinOptOutDialog.show(requireContext(), false, () -> requireActivity().finish()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java index 1f91e41380..5f8b85b2cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java @@ -17,7 +17,11 @@ public final class PinOptOutDialog { private static final String TAG = Log.tag(PinOptOutDialog.class); - public static void show(@NonNull Context context, @NonNull Runnable onSuccess) { + /** + * @param rotateAep If true, this will rotate the AEP as part of the process of opting out. Only do this if the user has not enabled backups! If the user + * has backups enabled, you should guide them through rotating the AEP first, and then call this with [rotateAep] = false. + */ + public static void show(@NonNull Context context, boolean rotateAep, @NonNull Runnable onSuccess) { Log.i(TAG, "show()"); AlertDialog dialog = new MaterialAlertDialogBuilder(context) .setTitle(R.string.PinOptOutDialog_warning) @@ -29,7 +33,7 @@ public final class PinOptOutDialog { AlertDialog progress = SimpleProgressDialog.show(context); SimpleTask.run(() -> { - SvrRepository.optOutOfPin(); + SvrRepository.optOutOfPin(rotateAep); return null; }, success -> { Log.i(TAG, "Disable operation finished."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt index b38c10f4e8..f11e835ad4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/SvrRepository.kt @@ -15,6 +15,7 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.JobTracker +import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob import org.thoughtcrime.securesms.jobs.StorageForcePushJob @@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.megaphone.Megaphones import org.thoughtcrime.securesms.net.SignalNetwork import org.thoughtcrime.securesms.registration.ui.restore.StorageServiceRestore import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet +import org.whispersystems.signalservice.api.AccountEntropyPool import org.whispersystems.signalservice.api.NetworkResultUtil import org.whispersystems.signalservice.api.SvrNoDataException import org.whispersystems.signalservice.api.kbs.MasterKey @@ -357,12 +359,21 @@ object SvrRepository { } } + /** + * @param rotateAep If true, this will rotate the AEP as part of the process of opting out. Only do this if the user has not enabled backups! If the user + * has backups enabled, you should guide them through rotating the AEP first, and then call this with [rotateAep] = false. + */ @JvmStatic @WorkerThread - fun optOutOfPin() { + fun optOutOfPin(rotateAep: Boolean) { operationLock.withLock { SignalStore.svr.optOut() + if (rotateAep) { + SignalStore.account.rotateAccountEntropyPool(AccountEntropyPool.generate()) + AppDependencies.jobManager.add(MultiDeviceKeysUpdateJob()) + } + AppDependencies.megaphoneRepository.markFinished(Megaphones.Event.PINS_FOR_ALL) bestEffortRefreshAttributes() diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsFragment.kt index 5aa91e6872..749e7bd325 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsFragment.kt @@ -18,11 +18,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -33,11 +35,13 @@ import org.signal.core.ui.compose.Rows import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.Snackbars import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity import org.thoughtcrime.securesms.payments.backup.PaymentsRecoveryStartFragmentArgs.Builder import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity import org.thoughtcrime.securesms.pin.PinOptOutDialog +import org.thoughtcrime.securesms.util.navigation.safeNavigate /** * Fragment which allows user to enable or disable their PIN @@ -56,7 +60,7 @@ class AdvancedPinSettingsFragment : ComposeFragment() { repeatOnLifecycle(Lifecycle.State.RESUMED) { viewModel.event.collectLatest { when (it) { - AdvancedPinSettingsViewModel.Event.SHOW_OPT_OUT_DIALOG -> PinOptOutDialog.show(requireContext()) { + AdvancedPinSettingsViewModel.Event.SHOW_BACKUPS_DISABLED_OPT_OUT_DIALOG -> PinOptOutDialog.show(requireContext(), true) { viewModel.onPinOptOutSuccess() displayOptOutSnackbar() } @@ -70,10 +74,20 @@ class AdvancedPinSettingsFragment : ComposeFragment() { startActivity(intent) } + AdvancedPinSettingsViewModel.Event.SHOW_PIN_DISABLED_SNACKBAR -> { + displayOptOutSnackbar() + } } } } } + + setFragmentResultListener(BackupKeyDisplayFragment.AEP_ROTATION_KEY) { key, bundle -> + val didRotate = bundle.getBoolean(BackupKeyDisplayFragment.AEP_ROTATION_KEY, false) + if (didRotate) { + viewModel.onAepRotatedForPinDisable() + } + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -109,7 +123,22 @@ class AdvancedPinSettingsFragment : ComposeFragment() { viewModel.dismissDialog() } ) - else -> Unit + AdvancedPinSettingsViewModel.Dialog.ROTATE_AEP -> RotateAepDialog( + onConfirm = { + viewModel.dismissDialog() + val bundle = Bundle() + bundle.putBoolean("start_with_key_rotation", true) + findNavController().safeNavigate( + AdvancedPinSettingsFragmentDirections + .actionAdvancedPinSettingsFragmentToBackupKeyDisplayFragment() + .setStartWithKeyRotation(true) + ) + }, + onDismiss = { + viewModel.dismissDialog() + } + ) + AdvancedPinSettingsViewModel.Dialog.NONE -> Unit } } @@ -191,6 +220,21 @@ private fun RecordPaymentsRecoveryPhraseDialog( ) } +@Composable +private fun RotateAepDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.AdvancedPinSettingsFragment_rotate_aep_dialog_title), + body = stringResource(R.string.AdvancedPinSettingsFragment_rotate_aep_dialog_body), + confirm = stringResource(R.string.AdvancedPinSettingsFragment_rotate_aep_dialog_positive_button), + onConfirm = onConfirm, + dismiss = stringResource(android.R.string.cancel), + onDismiss = onDismiss + ) +} + @DayNightPreviews @Composable private fun AdvancedPinSettingsFragmentContentEnabledPreview() { @@ -226,3 +270,11 @@ private fun RecordPaymentsRecoveryPhraseDialogPreview() { RecordPaymentsRecoveryPhraseDialog({}, {}) } } + +@DayNightPreviews +@Composable +private fun RotateAepDialogPreview() { + Previews.Preview { + RotateAepDialog({}, {}) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsViewModel.kt index 93dd3188e7..aa7006e585 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinSettingsViewModel.kt @@ -14,19 +14,22 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.SvrRepository class AdvancedPinSettingsViewModel : ViewModel() { enum class Dialog { NONE, REGISTRATION_LOCK, - RECORD_PAYMENTS_RECOVERY_PHRASE + RECORD_PAYMENTS_RECOVERY_PHRASE, + ROTATE_AEP } enum class Event { - SHOW_OPT_OUT_DIALOG, + SHOW_BACKUPS_DISABLED_OPT_OUT_DIALOG, LAUNCH_PIN_CREATION_FLOW, - LAUNCH_RECOVERY_PHRASE_HANDLING + LAUNCH_RECOVERY_PHRASE_HANDLING, + SHOW_PIN_DISABLED_SNACKBAR } private val internalDialog = MutableStateFlow(Dialog.NONE) @@ -52,9 +55,12 @@ class AdvancedPinSettingsViewModel : ViewModel() { !enabled && SignalStore.payments.mobileCoinPaymentsEnabled() && !SignalStore.payments.userConfirmedMnemonic -> { internalDialog.value = Dialog.RECORD_PAYMENTS_RECOVERY_PHRASE } - !enabled -> { + !enabled && SignalStore.backup.areBackupsEnabled -> { + internalDialog.value = Dialog.ROTATE_AEP + } + !enabled && !SignalStore.backup.areBackupsEnabled -> { dismissDialog() - emitEvent(Event.SHOW_OPT_OUT_DIALOG) + emitEvent(Event.SHOW_BACKUPS_DISABLED_OPT_OUT_DIALOG) } else -> { dismissDialog() @@ -75,6 +81,14 @@ class AdvancedPinSettingsViewModel : ViewModel() { internalDialog.value = Dialog.NONE } + fun onAepRotatedForPinDisable() { + internalDialog.value = Dialog.NONE + viewModelScope.launch { + SvrRepository.optOutOfPin(rotateAep = false) + emitEvent(Event.SHOW_PIN_DISABLED_SNACKBAR) + } + } + private fun emitEvent(event: Event) { viewModelScope.launch { internalEvent.emit(event) diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index b6b356f2b8..8444d4f198 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -209,7 +209,15 @@ + android:label="advanced_pin_settings_fragment"> + + + android:name="org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyDisplayFragment"> + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 87ed5de497..0212bf2cd2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -61,6 +61,12 @@ Record payments recovery phrase Record phrase Before you can disable your PIN, you must record your payments recovery phrase to ensure you can recover your payments account. + + Create a new recovery key + + You must create and record a new recovery key before you can disable your PIN. + + Create key 1