Prompt users to backup during quick restore.

This commit is contained in:
Greyson Parrelli
2026-01-14 10:51:29 -05:00
committed by Alex Hart
parent 4921198cd8
commit 9012a2afc0
26 changed files with 1049 additions and 245 deletions

View File

@@ -68,7 +68,7 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
AppSettingsRoute.LinkDeviceRoute.LinkDevice -> AppSettingsFragmentDirections.actionDirectToDevices()
AppSettingsRoute.UsernameLinkRoute.UsernameLink -> AppSettingsFragmentDirections.actionDirectToUsernameLinkSettings()
is AppSettingsRoute.AccountRoute.Username -> AppSettingsFragmentDirections.actionDirectToUsernameRecovery()
is AppSettingsRoute.BackupsRoute.Remote -> AppSettingsFragmentDirections.actionDirectToRemoteBackupsSettingsFragment()
is AppSettingsRoute.BackupsRoute.Remote -> AppSettingsFragmentDirections.actionDirectToRemoteBackupsSettingsFragment().setForQuickRestore(appSettingsRoute.forQuickRestore)
AppSettingsRoute.ChatFoldersRoute.ChatFolders -> AppSettingsFragmentDirections.actionDirectToChatFoldersFragment()
is AppSettingsRoute.ChatFoldersRoute.CreateChatFolders -> AppSettingsFragmentDirections.actionDirectToCreateFoldersFragment(
appSettingsRoute.folderId,
@@ -204,7 +204,8 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
fun usernameRecovery(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.AccountRoute.Username(mode = UsernameEditMode.RECOVERY))
@JvmStatic
fun remoteBackups(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Remote())
@JvmOverloads
fun remoteBackups(context: Context, forQuickRestore: Boolean = false): Intent = getIntentForStartLocation(context, AppSettingsRoute.BackupsRoute.Remote(forQuickRestore = forQuickRestore))
@JvmStatic
fun chatFolders(context: Context): Intent = getIntentForStartLocation(context, AppSettingsRoute.ChatFoldersRoute.ChatFolders)

View File

@@ -37,11 +37,13 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
@@ -49,6 +51,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -71,6 +74,7 @@ import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
@@ -113,6 +117,7 @@ import org.thoughtcrime.securesms.compose.StatusBarColorNestedScrollConnection
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.DateUtils
@@ -186,7 +191,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onBackupNowClick() {
viewModel.onBackupNowClick()
viewModel.onBackupNowClick(args.forQuickRestore)
}
override fun onTurnOffAndDeleteBackupsClick() {
@@ -295,6 +300,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
override fun onFreeTierBackupSizeLearnMore() {
CommunicationActions.openBrowserLink(requireContext(), "https://support.signal.org/hc/articles/9708267671322")
}
override fun onTransferScanQrCodeClick() {
startActivity(MediaSelectionActivity.camera(context!!))
}
}
private fun displayBackupKey() {
@@ -400,6 +409,7 @@ private interface ContentCallbacks {
fun onIncludeDebuglogClick(newState: Boolean) = Unit
fun onMediaBackupSizeClick() = Unit
fun onFreeTierBackupSizeLearnMore() = Unit
fun onTransferScanQrCodeClick() = Unit
object Empty : ContentCallbacks
}
@@ -668,6 +678,13 @@ private fun RemoteBackupsSettingsContent(
onDismiss = contentCallbacks::onDialogDismissed
)
}
RemoteBackupsSettingsState.Dialog.READY_TO_TRANSFER -> {
ReadyToTransferBottomSheet(
onScanQrCodeClick = contentCallbacks::onTransferScanQrCodeClick,
onDismiss = contentCallbacks::onDialogDismissed
)
}
}
val snackbarMessageId = remember(state.snackbar) {
@@ -1699,6 +1716,90 @@ private fun ResumeRestoreOverCellularDialog(
)
}
/**
* Bottom sheet displayed when the device is ready to transfer data to a new device.
* Shows a QR code illustration and prompts the user to scan the QR code on the target device.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ReadyToTransferBottomSheet(
onScanQrCodeClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss,
dragHandle = { BottomSheets.Handle() }
) {
ReadyToTransferContent(
onScanQrCodeClick = onScanQrCodeClick,
onCancelClick = onDismiss
)
}
}
@Composable
private fun ReadyToTransferContent(
onScanQrCodeClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 40.dp)
) {
Spacer(modifier = Modifier.size(16.dp))
Image(
painter = painterResource(R.drawable.illustration_scan_qr_transfer),
contentDescription = null,
modifier = Modifier.size(192.dp)
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__ready_to_transfer),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier.horizontalGutters()
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__use_this_device_to_scan_qr),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.horizontalGutters()
)
Spacer(modifier = Modifier.size(36.dp))
Buttons.LargeTonal(
onClick = onScanQrCodeClick,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
) {
Text(text = stringResource(R.string.RemoteBackupsSettingsFragment__scan_qr_code))
}
TextButton(
onClick = onCancelClick,
modifier = Modifier.padding(top = 8.dp)
) {
Text(
text = stringResource(android.R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
}
}
}
@Composable
private fun BackupReadyToDownloadRow(
ready: BackupRestoreState.Ready,
@@ -2156,6 +2257,14 @@ private fun SkipDownloadDialogPreview() {
}
}
@DayNightPreviews
@Composable
private fun ReadyToTransferContentPreview() {
Previews.BottomSheetPreview {
ReadyToTransferContent()
}
}
@DayNightPreviews
@Composable
private fun BackupDeletionCardPreview() {

View File

@@ -55,7 +55,8 @@ data class RemoteBackupsSettingsState(
CANCEL_MEDIA_RESTORE_PROTECTION,
RESTORE_OVER_CELLULAR_PROTECTION,
FREE_TIER_MEDIA_EXPLAINER,
KEY_ROTATION_LIMIT_REACHED
KEY_ROTATION_LIMIT_REACHED,
READY_TO_TRANSFER
}
enum class Snackbar {

View File

@@ -88,6 +88,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
val state: StateFlow<RemoteBackupsSettingsState> = _state
val restoreState: StateFlow<BackupRestoreState> = _restoreState
private var forQuickRestore = false
init {
ArchiveUploadProgress.triggerUpdate()
@@ -172,6 +174,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
.collect { current ->
if (previous != null && previous != current.state && current.state == ArchiveUploadProgressState.State.None) {
Log.d(TAG, "Refreshing state after archive upload.")
if (forQuickRestore) {
Log.d(TAG, "Backup completed with the forQuickRestore flag on. Refreshing state.")
_state.value = _state.value.copy(dialog = RemoteBackupsSettingsState.Dialog.READY_TO_TRANSFER)
}
refreshState(null)
}
previous = current.state
@@ -275,8 +281,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
}
fun onBackupNowClick() {
fun onBackupNowClick(forQuickRestore: Boolean) {
BackupMessagesJob.enqueue()
this.forQuickRestore = forQuickRestore
}
fun cancelUpload() {

View File

@@ -65,7 +65,7 @@ sealed interface AppSettingsRoute : Parcelable {
sealed interface BackupsRoute : AppSettingsRoute {
data object Backups : BackupsRoute
data object Local : BackupsRoute
data class Remote(val backupLaterSelected: Boolean = false) : BackupsRoute
data class Remote(val backupLaterSelected: Boolean = false, val forQuickRestore: Boolean = false) : BackupsRoute
data object DisplayKey : BackupsRoute
}