mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Allow user to rotate AEP.
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user