Allow user to rotate AEP.

This commit is contained in:
Alex Hart
2025-07-18 10:36:49 -03:00
committed by GitHub
parent a9455b95ac
commit 36de1284c7
10 changed files with 457 additions and 44 deletions

View File

@@ -5,13 +5,28 @@
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.compose.Dialogs
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordMode
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyVerifyScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.compose.Nav
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
import org.thoughtcrime.securesms.util.viewModel
@@ -22,6 +37,7 @@ import org.thoughtcrime.securesms.util.viewModel
class BackupKeyDisplayFragment : ComposeFragment() {
companion object {
const val AEP_ROTATION_KEY = "AEP_ROTATION_KEY"
const val CLIPBOARD_TIMEOUT_SECONDS = 60
}
@@ -32,17 +48,108 @@ class BackupKeyDisplayFragment : ComposeFragment() {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val passwordManagerSettingsIntent = AndroidCredentialRepository.getCredentialManagerSettingsIntent(requireContext())
MessageBackupsKeyRecordScreen(
backupKey = SignalStore.account.accountEntropyPool.displayValue,
keySaveState = state.keySaveState,
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
onNavigationClick = { findNavController().popBackStack() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
onNextClick = { findNavController().popBackStack() },
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) }
)
val navController = rememberNavController()
LaunchedEffect(Unit) {
navController.setLifecycleOwner(this@BackupKeyDisplayFragment)
navController.enableOnBackPressed(true)
}
LaunchedEffect(state.rotationState) {
if (state.rotationState == BackupKeyRotationState.FINISHED) {
setFragmentResult(AEP_ROTATION_KEY, bundleOf(AEP_ROTATION_KEY to true))
findNavController().popBackStack()
}
}
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
var displayWarningDialog by remember { mutableStateOf(false) }
BackHandler(enabled = state.rotationState == BackupKeyRotationState.USER_VERIFICATION) {
displayWarningDialog = true
}
val mode = remember(state.rotationState) {
if (state.rotationState == BackupKeyRotationState.NOT_STARTED) {
MessageBackupsKeyRecordMode.CreateNewKey(
onCreateNewKeyClick = {
viewModel.rotateBackupKey()
},
onTurnOffAndDownloadClick = {
viewModel.turnOffOptimizedStorageAndDownloadMedia()
findNavController().popBackStack()
},
isOptimizedStorageEnabled = state.isOptimizedStorageEnabled
)
} else {
MessageBackupsKeyRecordMode.Next(
onNextClick = {
navController.navigate(Screen.Verify.route)
}
)
}
}
if (state.rotationState == BackupKeyRotationState.GENERATING_KEY || state.rotationState == BackupKeyRotationState.COMMITTING_KEY) {
Dialogs.IndeterminateProgressDialog()
}
if (displayWarningDialog) {
BackupKeyNotCommitedWarningDialog(
onConfirm = {
findNavController().popBackStack()
},
onCancel = {
displayWarningDialog = false
navController.navigate(Screen.Verify.route)
}
)
}
Nav.Host(
navController = navController,
startDestination = Screen.Record.route
) {
composable(Screen.Record.route) {
MessageBackupsKeyRecordScreen(
backupKey = state.accountEntropyPool.displayValue,
keySaveState = state.keySaveState,
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
onNavigationClick = { onBackPressedDispatcher?.onBackPressed() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
mode = mode,
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) }
)
}
composable(Screen.Verify.route) {
MessageBackupsKeyVerifyScreen(
backupKey = state.accountEntropyPool.displayValue,
onNavigationClick = { onBackPressedDispatcher?.onBackPressed() },
onNextClick = { viewModel.commitBackupKey() }
)
}
}
}
}
@Composable
private fun BackupKeyNotCommitedWarningDialog(
onConfirm: () -> Unit,
onCancel: () -> Unit
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.BackupKeyDisplayFragment__cancel_key_creation_question),
body = stringResource(R.string.BackupKeyDisplayFragment__your_new_backup_key),
confirm = stringResource(R.string.BackupKeyDisplayFragment__cancel_key_creation),
dismiss = stringResource(R.string.BackupKeyDisplayFragment__confirm_key),
onConfirm = onConfirm,
onDeny = onCancel
)
}
private enum class Screen(val route: String) {
Record("record-screen"),
Verify("verify-screen")
}

View File

@@ -6,10 +6,19 @@
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.concurrent.SignalDispatchers
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.AccountEntropyPool
class BackupKeyDisplayViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
private val _uiState = MutableStateFlow(BackupKeyDisplayUiState())
@@ -18,8 +27,54 @@ class BackupKeyDisplayViewModel : ViewModel(), BackupKeyCredentialManagerHandler
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {
_uiState.update { it.copy(keySaveState = newState) }
}
fun rotateBackupKey() {
viewModelScope.launch {
_uiState.update { it.copy(rotationState = BackupKeyRotationState.GENERATING_KEY) }
val stagedAEP = withContext(SignalDispatchers.IO) {
BackupRepository.stageAEPKeyRotation()
}
_uiState.update {
it.copy(
accountEntropyPool = stagedAEP,
rotationState = BackupKeyRotationState.USER_VERIFICATION
)
}
}
}
fun commitBackupKey() {
viewModelScope.launch {
_uiState.update { it.copy(rotationState = BackupKeyRotationState.COMMITTING_KEY) }
withContext(SignalDispatchers.IO) {
BackupRepository.commitAEPKeyRotation(_uiState.value.accountEntropyPool)
}
_uiState.update { it.copy(rotationState = BackupKeyRotationState.FINISHED) }
}
}
fun turnOffOptimizedStorageAndDownloadMedia() {
SignalStore.backup.optimizeStorage = false
// TODO - flag to notify when complete.
AppDependencies.jobManager.add(RestoreOptimizedMediaJob())
}
}
data class BackupKeyDisplayUiState(
val keySaveState: BackupKeySaveState? = null
val accountEntropyPool: AccountEntropyPool = SignalStore.account.accountEntropyPool,
val keySaveState: BackupKeySaveState? = null,
val isOptimizedStorageEnabled: Boolean = SignalStore.backup.optimizeStorage,
val rotationState: BackupKeyRotationState = BackupKeyRotationState.NOT_STARTED
)
enum class BackupKeyRotationState {
NOT_STARTED,
GENERATING_KEY,
USER_VERIFICATION,
COMMITTING_KEY,
FINISHED
}

View File

@@ -70,6 +70,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@@ -304,6 +305,13 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
}
setFragmentResultListener(BackupKeyDisplayFragment.AEP_ROTATION_KEY) { _, bundle ->
val didRotate = bundle.getBoolean(BackupKeyDisplayFragment.AEP_ROTATION_KEY, false)
if (didRotate) {
viewModel.requestSnackbar(RemoteBackupsSettingsState.Snackbar.AEP_KEY_ROTATED)
}
}
if (savedInstanceState == null && args.backupLaterSelected) {
viewModel.requestSnackbar(RemoteBackupsSettingsState.Snackbar.BACKUP_WILL_BE_CREATED_OVERNIGHT)
}
@@ -628,6 +636,7 @@ private fun RemoteBackupsSettingsContent(
RemoteBackupsSettingsState.Snackbar.SUBSCRIPTION_CANCELLED -> R.string.RemoteBackupsSettingsFragment__subscription_cancelled
RemoteBackupsSettingsState.Snackbar.DOWNLOAD_COMPLETE -> R.string.RemoteBackupsSettingsFragment__download_complete
RemoteBackupsSettingsState.Snackbar.BACKUP_WILL_BE_CREATED_OVERNIGHT -> R.string.RemoteBackupsSettingsFragment__backup_will_be_created_overnight
RemoteBackupsSettingsState.Snackbar.AEP_KEY_ROTATED -> R.string.RemoteBackupsSettingsFragment__new_backup_key_created
}
}

View File

@@ -50,6 +50,7 @@ data class RemoteBackupsSettingsState(
BACKUP_TYPE_CHANGED_AND_SUBSCRIPTION_CANCELLED,
SUBSCRIPTION_CANCELLED,
DOWNLOAD_COMPLETE,
BACKUP_WILL_BE_CREATED_OVERNIGHT
BACKUP_WILL_BE_CREATED_OVERNIGHT,
AEP_KEY_ROTATED
}
}