mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 03:40:56 +01: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) }
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user