diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt
index feda926ac0..e592ace6c5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt
@@ -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.
*/
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
index 68e092d909..b45ab9ec81 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt
@@ -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,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt
index 990ecaca51..cef7562daf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsKeyRecordScreen.kt
@@ -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,19 +191,23 @@ fun MessageBackupsKeyRecordScreen(
}
}
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .padding(bottom = 24.dp)
- ) {
- Buttons.LargeTonal(
- onClick = onNextClick,
- modifier = Modifier.align(Alignment.BottomEnd)
+ if (mode is MessageBackupsKeyRecordMode.Next) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 24.dp)
) {
- Text(
- text = stringResource(R.string.MessageBackupsKeyRecordScreen__next)
- )
+ Buttons.LargeTonal(
+ onClick = mode.onNextClick,
+ modifier = Modifier.align(Alignment.BottomEnd)
+ ) {
+ Text(
+ text = stringResource(R.string.MessageBackupsKeyRecordScreen__next)
+ )
+ }
}
+ } else if (mode is MessageBackupsKeyRecordMode.CreateNewKey) {
+ CreateNewKeyButton(mode)
}
}
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/verify/ForgotBackupKeyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/verify/ForgotBackupKeyFragment.kt
index dc5b9455a3..91d2a76242 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/verify/ForgotBackupKeyFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/verify/ForgotBackupKeyFragment.kt
@@ -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 = {
- requireActivity()
- .supportFragmentManager
- .beginTransaction()
- .add(R.id.content, ConfirmBackupKeyDisplayFragment())
- .addToBackStack(null)
- .commit()
+ mode = remember {
+ MessageBackupsKeyRecordMode.Next(onNextClick = {
+ requireActivity()
+ .supportFragmentManager
+ .beginTransaction()
+ .add(R.id.content, ConfirmBackupKeyDisplayFragment())
+ .addToBackStack(null)
+ .commit()
+ })
},
onGoToPasswordManagerSettingsClick = { requireContext().startActivity(passwordManagerSettingsIntent) }
)
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 e07f2aeb1a..a181a8f229 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
@@ -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")
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt
index 0f6e2c012f..b8096298e7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupKeyDisplayViewModel.kt
@@ -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
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt
index bcc4c5d61b..fcea8563e1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt
@@ -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
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt
index d101d5a762..5cb6dcafda 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt
@@ -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
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt
index e990a01719..dcf5ee76d5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/AccountValues.kt
@@ -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
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9b8722d839..54c0ad3c3c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8156,6 +8156,8 @@
Download complete
Backup will be created overnight.
+
+ New backup key created
Subscription inactive
@@ -8405,6 +8407,28 @@
Continue
See key again
+
+ Create new key
+
+ Download media
+
+ 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.
+
+ Turn off and download
+
+ Create a new backup key
+
+ 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.
+
+
+
+ Cancel key creation?
+
+ Your new backup key won\'t be created unless you confirm it.
+
+ Confirm key
+
+ Cancel key creation
Confirm your backup key