mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 20:18:36 +00:00
Allow user to rotate AEP.
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user