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

@@ -9,10 +9,12 @@ import android.app.PendingIntent
import android.database.Cursor
import android.os.Environment
import android.os.StatFs
import androidx.annotation.CheckResult
import androidx.annotation.Discouraged
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import okio.ByteString
@@ -97,9 +99,11 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
import org.thoughtcrime.securesms.jobs.BackupDeleteJob
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.CheckRestoreMediaLeftJob
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
import org.thoughtcrime.securesms.jobs.LocalBackupJob
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
@@ -175,10 +179,7 @@ object BackupRepository {
when (error.code) {
401 -> {
Log.w(TAG, "Received status 401. Resetting initialized state + auth credentials.", error.exception)
SignalStore.backup.backupsInitialized = false
SignalStore.backup.messageCredentials.clearAll()
SignalStore.backup.mediaCredentials.clearAll()
SignalStore.backup.cachedMediaCdnPath = null
resetInitializedStateAndAuthCredentials()
}
403 -> {
@@ -207,6 +208,41 @@ object BackupRepository {
}
}
/**
* Generates a new AEP that the user can choose to confirm.
*/
@CheckResult
fun stageAEPKeyRotation(): AccountEntropyPool {
return AccountEntropyPool.generate()
}
/**
* Saves the AEP to the local storage and kicks off a backup upload.
*/
suspend fun commitAEPKeyRotation(accountEntropyPool: AccountEntropyPool) {
haltAllJobs()
resetInitializedStateAndAuthCredentials()
SignalStore.account.rotateAccountEntropyPool(accountEntropyPool)
BackupMessagesJob.enqueue()
}
private fun resetInitializedStateAndAuthCredentials() {
SignalStore.backup.backupsInitialized = false
SignalStore.backup.messageCredentials.clearAll()
SignalStore.backup.mediaCredentials.clearAll()
SignalStore.backup.cachedMediaCdnPath = null
}
private suspend fun haltAllJobs() {
ArchiveUploadProgress.cancelAndBlock()
AppDependencies.jobManager.cancelAllInQueue(LocalBackupJob.QUEUE)
Log.d(TAG, "Waiting for local backup job cancelations to occur...")
while (!AppDependencies.jobManager.areQueuesEmpty(setOf(LocalBackupJob.QUEUE))) {
delay(1.seconds)
}
}
/**
* Triggers backup id reservation. As documented, this is safe to perform multiple times.
*/

View File

@@ -14,6 +14,7 @@ import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
@@ -147,7 +148,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
keySaveState = state.backupKeySaveState,
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
onNavigationClick = viewModel::goToPreviousStage,
onNextClick = viewModel::goToNextStage,
mode = remember { MessageBackupsKeyRecordMode.Next(viewModel::goToNextStage) },
onCopyToClipboardClick = { Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,

View File

@@ -11,29 +11,39 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidthIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.compose.Buttons
@@ -42,6 +52,7 @@ import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
@@ -51,6 +62,16 @@ import org.thoughtcrime.securesms.util.storage.CredentialManagerError
import org.thoughtcrime.securesms.util.storage.CredentialManagerResult
import org.signal.core.ui.R as CoreUiR
@Stable
sealed interface MessageBackupsKeyRecordMode {
data class Next(val onNextClick: () -> Unit) : MessageBackupsKeyRecordMode
data class CreateNewKey(
val onCreateNewKeyClick: () -> Unit,
val onTurnOffAndDownloadClick: () -> Unit,
val isOptimizedStorageEnabled: Boolean
) : MessageBackupsKeyRecordMode
}
/**
* Screen displaying the backup key allowing the user to write it down
* or copy it.
@@ -65,8 +86,8 @@ fun MessageBackupsKeyRecordScreen(
onRequestSaveToPasswordManager: () -> Unit = {},
onConfirmSaveToPasswordManager: () -> Unit = {},
onSaveToPasswordManagerComplete: (CredentialManagerResult) -> Unit = {},
onNextClick: () -> Unit = {},
onGoToPasswordManagerSettingsClick: () -> Unit = {}
onGoToPasswordManagerSettingsClick: () -> Unit = {},
mode: MessageBackupsKeyRecordMode = MessageBackupsKeyRecordMode.Next(onNextClick = {})
) {
val snackbarHostState = remember { SnackbarHostState() }
val backupKeyString = remember(backupKey) {
@@ -96,7 +117,7 @@ fun MessageBackupsKeyRecordScreen(
) {
item {
Image(
painter = painterResource(R.drawable.image_signal_backups_lock),
imageVector = ImageVector.vectorResource(R.drawable.image_signal_backups_lock),
contentDescription = null,
modifier = Modifier
.padding(top = 24.dp)
@@ -170,13 +191,14 @@ fun MessageBackupsKeyRecordScreen(
}
}
if (mode is MessageBackupsKeyRecordMode.Next) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
Buttons.LargeTonal(
onClick = onNextClick,
onClick = mode.onNextClick,
modifier = Modifier.align(Alignment.BottomEnd)
) {
Text(
@@ -184,6 +206,9 @@ fun MessageBackupsKeyRecordScreen(
)
}
}
} else if (mode is MessageBackupsKeyRecordMode.CreateNewKey) {
CreateNewKeyButton(mode)
}
}
when (keySaveState) {
@@ -226,6 +251,51 @@ fun MessageBackupsKeyRecordScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CreateNewKeyButton(
mode: MessageBackupsKeyRecordMode.CreateNewKey
) {
var displayBottomSheet by remember { mutableStateOf(false) }
var displayDialog by remember { mutableStateOf(false) }
TextButton(
onClick = { displayBottomSheet = true },
modifier = Modifier
.padding(bottom = 24.dp)
.horizontalGutters()
.fillMaxWidth()
.requiredWidthIn(min = Dp.Unspecified, max = 264.dp)
) {
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__create_new_key))
}
if (displayDialog) {
DownloadMediaDialog(
onTurnOffAndDownloadClick = mode.onTurnOffAndDownloadClick,
onCancelClick = { displayDialog = false }
)
}
if (displayBottomSheet) {
ModalBottomSheet(
onDismissRequest = { displayBottomSheet = false },
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
) {
CreateNewBackupKeySheetContent(
onContinueClick = {
if (mode.isOptimizedStorageEnabled) {
displayDialog = true
} else {
mode.onCreateNewKeyClick()
}
},
onCancelClick = { displayBottomSheet = false }
)
}
}
}
@Composable
private fun BackupKeySaveErrorDialog(
error: BackupKeySaveState.Error,
@@ -268,6 +338,78 @@ private fun BackupKeySaveErrorDialog(
}
}
@Composable
private fun ColumnScope.CreateNewBackupKeySheetContent(
onContinueClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.image_signal_backups_key),
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 38.dp, bottom = 18.dp)
.size(80.dp)
)
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__create_a_new_backup_key),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 12.dp)
.horizontalGutters()
)
Text(
text = stringResource(R.string.MessageBackupsKeyRecordScreen__creating_a_new_key_is_only_necessary),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(bottom = 91.dp, start = 36.dp, end = 36.dp)
.align(Alignment.CenterHorizontally)
)
Buttons.LargeTonal(
onClick = onContinueClick,
modifier = Modifier
.padding(bottom = 16.dp)
.fillMaxWidth()
.requiredWidthIn(min = Dp.Unspecified, max = 220.dp)
.align(Alignment.CenterHorizontally)
) {
Text(text = stringResource(R.string.MessageBackupsKeyRecordScreen__continue))
}
TextButton(
onClick = onCancelClick,
modifier = Modifier
.padding(bottom = 48.dp)
.fillMaxWidth()
.requiredWidthIn(min = Dp.Unspecified, max = 220.dp)
.align(Alignment.CenterHorizontally)
) {
Text(text = stringResource(android.R.string.cancel))
}
}
@Composable
private fun DownloadMediaDialog(
onTurnOffAndDownloadClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.MessageBackupsKeyRecordScreen__download_media),
body = stringResource(R.string.MessageBackupsKeyRecordScreen__to_create_a_new_backup_key),
confirm = stringResource(R.string.MessageBackupsKeyRecordScreen__turn_off_and_download),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onTurnOffAndDownloadClick,
onDeny = onCancelClick
)
}
private suspend fun saveKeyToCredentialManager(
@UiContext activityContext: Context,
backupKey: String
@@ -286,7 +428,12 @@ private fun MessageBackupsKeyRecordScreenPreview() {
MessageBackupsKeyRecordScreen(
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0",
keySaveState = null,
canOpenPasswordManagerSettings = true
canOpenPasswordManagerSettings = true,
mode = MessageBackupsKeyRecordMode.CreateNewKey(
onCreateNewKeyClick = {},
onTurnOffAndDownloadClick = {},
isOptimizedStorageEnabled = true
)
)
}
}
@@ -298,7 +445,27 @@ private fun SaveKeyConfirmationDialogPreview() {
MessageBackupsKeyRecordScreen(
backupKey = (0 until 63).map { (('A'..'Z') + ('0'..'9')).random() }.joinToString("") + "0",
keySaveState = BackupKeySaveState.RequestingConfirmation,
canOpenPasswordManagerSettings = true
canOpenPasswordManagerSettings = true,
mode = MessageBackupsKeyRecordMode.Next(onNextClick = {})
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@SignalPreview
@Composable
private fun CreateNewBackupKeySheetContentPreview() {
Previews.BottomSheetPreview {
Column {
CreateNewBackupKeySheetContent()
}
}
}
@SignalPreview
@Composable
private fun DownloadMediaDialogPreview() {
Previews.Preview {
DownloadMediaDialog()
}
}

View File

@@ -3,7 +3,9 @@ package org.thoughtcrime.securesms.backup.v2.ui.verify
import android.R
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordMode
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsKeyRecordScreen
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -36,13 +38,15 @@ class ForgotBackupKeyFragment : ComposeFragment() {
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
onNextClick = {
mode = remember {
MessageBackupsKeyRecordMode.Next(onNextClick = {
requireActivity()
.supportFragmentManager
.beginTransaction()
.add(R.id.content, ConfirmBackupKeyDisplayFragment())
.addToBackStack(null)
.commit()
})
},
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) }
)

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())
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 = SignalStore.account.accountEntropyPool.displayValue,
backupKey = state.accountEntropyPool.displayValue,
keySaveState = state.keySaveState,
canOpenPasswordManagerSettings = passwordManagerSettingsIntent != null,
onNavigationClick = { findNavController().popBackStack() },
onNavigationClick = { onBackPressedDispatcher?.onBackPressed() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
onRequestSaveToPasswordManager = viewModel::onBackupKeySaveRequested,
onConfirmSaveToPasswordManager = viewModel::onBackupKeySaveConfirmed,
onSaveToPasswordManagerComplete = viewModel::onBackupKeySaveCompleted,
onNextClick = { findNavController().popBackStack() },
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
}
}

View File

@@ -141,6 +141,15 @@ class AccountValues internal constructor(store: KeyValueStore, context: Context)
}
}
fun rotateAccountEntropyPool(aep: AccountEntropyPool) {
AEP_LOCK.withLock {
store
.beginWrite()
.putString(KEY_ACCOUNT_ENTROPY_POOL, aep.value)
.commit()
}
}
fun restoreAccountEntropyPool(aep: AccountEntropyPool) {
AEP_LOCK.withLock {
store

View File

@@ -8156,6 +8156,8 @@
<string name="RemoteBackupsSettingsFragment__download_complete">Download complete</string>
<!-- Snackbar text displayed when backup will be created overnight -->
<string name="RemoteBackupsSettingsFragment__backup_will_be_created_overnight">Backup will be created overnight.</string>
<!-- Snackbar text displayed when user rotates AEP -->
<string name="RemoteBackupsSettingsFragment__new_backup_key_created">New backup key created</string>
<!-- Text displayed in card when subscription is inactive -->
<string name="RemoteBackupsSettingsFragment__subscription_inactive">Subscription inactive</string>
<!-- Text displayed in card when free tier is inactive -->
@@ -8405,6 +8407,28 @@
<string name="MessageBackupsKeyRecordScreen__continue">Continue</string>
<!-- Sheet secondary action button label -->
<string name="MessageBackupsKeyRecordScreen__see_key_again">See key again</string>
<!-- Action label to rotate AEP key -->
<string name="MessageBackupsKeyRecordScreen__create_new_key">Create new key</string>
<!-- Dialog title for confirming you need to disable optimized storage and download media to rotate AEP key -->
<string name="MessageBackupsKeyRecordScreen__download_media">Download media</string>
<!-- Dialog body for confirming you need to disable optimized storage and download media to rotate AEP key -->
<string name="MessageBackupsKeyRecordScreen__to_create_a_new_backup_key">To create a new backup key, you must turn off \"Optimize storage\" and wait until your media has been downloaded. When the download is complete, you can create a new key.</string>
<!-- Action label to turn off optimized storage and download media -->
<string name="MessageBackupsKeyRecordScreen__turn_off_and_download">Turn off and download</string>
<!-- Bottom Sheet title for creating a new AEP key -->
<string name="MessageBackupsKeyRecordScreen__create_a_new_backup_key">Create a new backup key</string>
<!-- Bottom Sheet body for creating a new AEP key -->
<string name="MessageBackupsKeyRecordScreen__creating_a_new_key_is_only_necessary">Creating a new key is only necessary if someone else knows your key. You will have to re-upload your backup, including media. If you are using \"Optimize storage\" you will have to download offloaded media first.</string>
<!-- BackupKeyDisplayFragment -->
<!-- Dialog title when exiting before confirming new key -->
<string name="BackupKeyDisplayFragment__cancel_key_creation_question">Cancel key creation?</string>
<!-- Dialog body when exiting before confirming new key -->
<string name="BackupKeyDisplayFragment__your_new_backup_key">Your new backup key won\'t be created unless you confirm it.</string>
<!-- Dialog button to continue confirming new backup key-->
<string name="BackupKeyDisplayFragment__confirm_key">Confirm key</string>
<!-- Dialog button to cancel new key creation -->
<string name="BackupKeyDisplayFragment__cancel_key_creation">Cancel key creation</string>
<!-- Confirm key title -->
<string name="MessageBackupsKeyVerifyScreen__confirm_your_backup_key">Confirm your backup key</string>