Implement stop/resume media restore and update restore over cellular.

This commit is contained in:
Cody Henthorne
2025-05-05 19:51:36 -04:00
committed by Michelle Tang
parent 9867fa3f50
commit 93403a0d2c
15 changed files with 183 additions and 46 deletions

View File

@@ -217,7 +217,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onCancelMediaRestore() {
viewModel.cancelMediaRestore()
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
}
override fun onDisplaySkipMediaRestoreProtectionDialog() {
@@ -245,8 +245,12 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(parentFragmentManager, null)
}
override fun onRestoreUsingCellularClick(canUseCellular: Boolean) {
viewModel.setCanRestoreUsingCellular(canUseCellular)
override fun onRestoreUsingCellularConfirm() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.RESTORE_OVER_CELLULAR_PROTECTION)
}
override fun onRestoreUsingCellularClick() {
viewModel.setCanRestoreUsingCellular()
}
override fun onRedemptionErrorDetailsClick() {
@@ -334,7 +338,8 @@ private interface ContentCallbacks {
fun onLearnMoreAboutLostSubscription() = Unit
fun onContactSupport() = Unit
fun onLearnMoreAboutBackupFailure() = Unit
fun onRestoreUsingCellularClick(canUseCellular: Boolean) = Unit
fun onRestoreUsingCellularConfirm() = Unit
fun onRestoreUsingCellularClick() = Unit
fun onRedemptionErrorDetailsClick() = Unit
}
@@ -425,18 +430,20 @@ private fun RemoteBackupsSettingsContent(
)
}
item {
Rows.ToggleRow(
checked = canRestoreUsingCellular,
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__restore_using_cellular),
onCheckChanged = contentCallbacks::onRestoreUsingCellularClick
)
if (!canRestoreUsingCellular) {
item {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
onClick = contentCallbacks::onRestoreUsingCellularConfirm
)
}
}
} else if (backupRestoreState is BackupRestoreState.Ready && backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
} else if (backupRestoreState is BackupRestoreState.Ready) {
item {
BackupReadyToDownloadRow(
ready = backupRestoreState,
endOfSubscription = backupState.renewalTime,
backupState = backupState,
onDownloadClick = contentCallbacks::onStartMediaRestore
)
}
@@ -536,6 +543,20 @@ private fun RemoteBackupsSettingsContent(
onSkipClick = contentCallbacks::onSkipMediaRestore
)
}
RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION -> {
CancelInitialRestoreDialog(
onDismiss = contentCallbacks::onDialogDismissed,
onSkipClick = contentCallbacks::onSkipMediaRestore
)
}
RemoteBackupsSettingsState.Dialog.RESTORE_OVER_CELLULAR_PROTECTION -> {
ResumeRestoreOverCellularDialog(
onDismiss = contentCallbacks::onDialogDismissed,
onResumeOverCellularClick = contentCallbacks::onRestoreUsingCellularClick
)
}
}
val snackbarMessageId = remember(requestedSnackbar) {
@@ -1164,6 +1185,37 @@ private fun SkipDownloadDialog(
)
}
@Composable
private fun CancelInitialRestoreDialog(
onSkipClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.RemoteBackupsSettingsFragment__skip_restore_question),
body = stringResource(R.string.RemoteBackupsSettingsFragment__skip_restore_message),
confirm = stringResource(R.string.RemoteBackupsSettingsFragment__skip),
dismiss = stringResource(android.R.string.cancel),
confirmColor = MaterialTheme.colorScheme.error,
onConfirm = onSkipClick,
onDismiss = onDismiss
)
}
@Composable
private fun ResumeRestoreOverCellularDialog(
onResumeOverCellularClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.ResumeRestoreCellular_resume_using_cellular_title),
body = stringResource(R.string.ResumeRestoreCellular_resume_using_cellular_message),
confirm = stringResource(R.string.BackupStatus__resume),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onResumeOverCellularClick,
onDismiss = onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CircularProgressDialog(
@@ -1254,11 +1306,16 @@ private fun BackupFrequencyDialog(
@Composable
private fun BackupReadyToDownloadRow(
ready: BackupRestoreState.Ready,
endOfSubscription: Duration,
backupState: RemoteBackupsSettingsState.BackupState,
onDownloadClick: () -> Unit = {}
) {
val days = (endOfSubscription - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
val string = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
val string = if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
val days = (backupState.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
} else {
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device, ready.bytes)
}
val annotated = buildAnnotatedString {
append(string)
val startIndex = string.indexOf(ready.bytes)
@@ -1436,7 +1493,7 @@ private fun BackupReadyToDownloadPreview() {
Previews.Preview {
BackupReadyToDownloadRow(
ready = BackupRestoreState.Ready("12GB"),
endOfSubscription = System.currentTimeMillis().milliseconds + 30.days
backupState = RemoteBackupsSettingsState.BackupState.None
)
}
}

View File

@@ -115,7 +115,9 @@ data class RemoteBackupsSettingsState(
DOWNLOADING_YOUR_BACKUP,
TURN_OFF_FAILED,
SUBSCRIPTION_NOT_FOUND,
SKIP_MEDIA_RESTORE_PROTECTION
SKIP_MEDIA_RESTORE_PROTECTION,
CANCEL_MEDIA_RESTORE_PROTECTION,
RESTORE_OVER_CELLULAR_PROTECTION
}
enum class Snackbar {

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -83,15 +84,25 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
}
viewModelScope.launch(Dispatchers.Default) {
viewModelScope.launch(Dispatchers.IO) {
val restoreProgress = MediaRestoreProgressBanner()
var optimizedRemainingBytes = 0L
while (isActive) {
if (restoreProgress.enabled) {
Log.d(TAG, "Backup is being restored. Collecting updates.")
restoreProgress.dataFlow.collectLatest { latest ->
_restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
}
restoreProgress
.dataFlow
.takeWhile { it !is BackupStatusData.RestoringMedia || it.restoreStatus != BackupStatusData.RestoreStatus.FINISHED }
.collectLatest { latest ->
_restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
}
} else if (
!SignalStore.backup.optimizeStorage &&
SignalStore.backup.userManuallySkippedMediaRestore &&
SignalDatabase.attachments.getOptimizedMediaAttachmentSize().also { optimizedRemainingBytes = it } > 0
) {
_restoreState.update { BackupRestoreState.Ready(optimizedRemainingBytes.bytes.toUnitString()) }
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
@@ -126,9 +137,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
_state.update { it.copy(canBackUpUsingCellular = canBackUpUsingCellular) }
}
fun setCanRestoreUsingCellular(canRestoreUsingCellular: Boolean) {
SignalStore.backup.restoreWithCellular = canRestoreUsingCellular
_state.update { it.copy(canRestoreUsingCellular = canRestoreUsingCellular) }
fun setCanRestoreUsingCellular() {
SignalStore.backup.restoreWithCellular = true
_state.update { it.copy(canRestoreUsingCellular = true) }
}
fun setBackupsFrequency(backupsFrequency: BackupFrequency) {
@@ -139,17 +150,13 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
fun beginMediaRestore() {
// TODO - [backups] Begin media restore.
BackupRepository.resumeMediaRestore()
}
fun skipMediaRestore() {
BackupRepository.skipMediaRestore()
}
fun cancelMediaRestore() {
// TODO - [backups] Cancel in-progress media restoration
}
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
_state.update { it.copy(dialog = dialog) }
}
@@ -269,14 +276,17 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
return
}
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
Log.d(TAG, "Found active signal subscription and active google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false
}
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
Log.d(TAG, "Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false
}
else -> {
Log.w(TAG, "Hit unexpected subscription mismatch state: signal:false, google:true")
return

View File

@@ -294,6 +294,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
fun wipeAllDataAndRestoreFromRemote() {
SignalExecutors.BOUNDED_IO.execute {
SignalStore.backup.restoreWithCellular = false
restoreFromRemote()
}
}