Improve backup subscription and restore UX for linked devices.

This commit is contained in:
Cody Henthorne
2026-09-15 18:26:04 -04:00
committed by Greyson Parrelli
parent 5a537d3ac0
commit 5a43ddf03e
3 changed files with 76 additions and 78 deletions
@@ -176,15 +176,12 @@ private fun BackupsSettingsContent(
}
item {
val displayActionButton = !backupsSettingsState.isLinkedDevice
when (backupsSettingsState.backupState) {
is BackupState.LocalStore -> {
LocalStoreBackupRow(
backupState = backupsSettingsState.backupState,
lastBackupAt = backupsSettingsState.lastBackupAt,
onBackupsRowClick = onBackupsRowClick,
displayActionButton = displayActionButton
onBackupsRowClick = onBackupsRowClick
)
OtherWaysToBackUpHeading()
@@ -195,8 +192,7 @@ private fun BackupsSettingsContent(
BackupsOffLinkedDeviceRow(onLearnMoreClick = onLearnMoreClick)
} else {
InactiveBackupsRow(
onBackupsRowClick = onBackupsRowClick,
displayActionButton = true
onBackupsRowClick = onBackupsRowClick
)
}
@@ -207,8 +203,7 @@ private fun BackupsSettingsContent(
ActiveBackupsRow(
backupState = backupsSettingsState.backupState,
onBackupsRowClick = onBackupsRowClick,
lastBackupAt = backupsSettingsState.lastBackupAt,
displayActionButton = true
lastBackupAt = backupsSettingsState.lastBackupAt
)
OtherWaysToBackUpHeading()
@@ -219,8 +214,7 @@ private fun BackupsSettingsContent(
BackupsOffLinkedDeviceRow(onLearnMoreClick = onLearnMoreClick)
} else {
NeverEnabledBackupsRow(
onBackupsRowClick = onBackupsRowClick,
displayActionButton = true
onBackupsRowClick = onBackupsRowClick
)
}
@@ -229,8 +223,7 @@ private fun BackupsSettingsContent(
is BackupState.Error -> {
WaitingForNetworkRow(
onBackupsRowClick = onBackupsRowClick,
displayActionButton = displayActionButton
onBackupsRowClick = onBackupsRowClick
)
OtherWaysToBackUpHeading()
@@ -238,8 +231,7 @@ private fun BackupsSettingsContent(
BackupState.NotFound -> {
NotFoundBackupRow(
onBackupsRowClick = onBackupsRowClick,
displayActionButton = displayActionButton
onBackupsRowClick = onBackupsRowClick
)
OtherWaysToBackUpHeading()
@@ -247,8 +239,7 @@ private fun BackupsSettingsContent(
is BackupState.Pending -> {
PendingBackupRow(
onBackupsRowClick = onBackupsRowClick,
displayActionButton = displayActionButton
onBackupsRowClick = onBackupsRowClick
)
OtherWaysToBackUpHeading()
@@ -258,8 +249,7 @@ private fun BackupsSettingsContent(
ActiveBackupsRow(
backupState = backupsSettingsState.backupState,
lastBackupAt = backupsSettingsState.lastBackupAt,
onBackupsRowClick = onBackupsRowClick,
displayActionButton = displayActionButton
onBackupsRowClick = onBackupsRowClick
)
OtherWaysToBackUpHeading()
@@ -290,8 +280,7 @@ private fun OtherWaysToBackUpHeading() {
@Composable
private fun NeverEnabledBackupsRow(
onBackupsRowClick: () -> Unit = {},
displayActionButton: Boolean = true
onBackupsRowClick: () -> Unit = {}
) {
Rows.TextRow(
modifier = Modifier.wrapContentHeight(),
@@ -320,15 +309,13 @@ private fun NeverEnabledBackupsRow(
style = MaterialTheme.typography.bodyMedium
)
if (displayActionButton) {
Buttons.MediumTonal(
onClick = onBackupsRowClick,
modifier = Modifier.padding(top = 12.dp)
) {
Text(
text = stringResource(R.string.BackupsSettingsFragment_set_up)
)
}
Buttons.MediumTonal(
onClick = onBackupsRowClick,
modifier = Modifier.padding(top = 12.dp)
) {
Text(
text = stringResource(R.string.BackupsSettingsFragment_set_up)
)
}
}
}
@@ -381,12 +368,12 @@ private fun BackupsOffLinkedDeviceRow(
}
@Composable
private fun WaitingForNetworkRow(onBackupsRowClick: () -> Unit = {}, displayActionButton: Boolean = true) {
private fun WaitingForNetworkRow(onBackupsRowClick: () -> Unit = {}) {
Rows.TextRow(
text = {
Column {
Text(text = stringResource(R.string.RemoteBackupsSettingsFragment__waiting_for_network))
ViewSettingsButton(onBackupsRowClick, displayActionButton)
ViewSettingsButton(onBackupsRowClick)
}
},
icon = {
@@ -397,8 +384,7 @@ private fun WaitingForNetworkRow(onBackupsRowClick: () -> Unit = {}, displayActi
@Composable
private fun InactiveBackupsRow(
onBackupsRowClick: () -> Unit = {},
displayActionButton: Boolean = true
onBackupsRowClick: () -> Unit = {}
) {
Rows.TextRow(
text = {
@@ -413,7 +399,7 @@ private fun InactiveBackupsRow(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
ViewSettingsButton(onBackupsRowClick, displayActionButton)
ViewSettingsButton(onBackupsRowClick)
}
},
icon = {
@@ -431,8 +417,7 @@ private fun InactiveBackupsRow(
@Composable
private fun NotFoundBackupRow(
onBackupsRowClick: () -> Unit = {},
displayActionButton: Boolean = true
onBackupsRowClick: () -> Unit = {}
) {
Rows.TextRow(
modifier = Modifier.wrapContentHeight(),
@@ -460,7 +445,7 @@ private fun NotFoundBackupRow(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
ViewSettingsButton(onBackupsRowClick, displayActionButton)
ViewSettingsButton(onBackupsRowClick)
}
}
)
@@ -468,8 +453,7 @@ private fun NotFoundBackupRow(
@Composable
private fun PendingBackupRow(
onBackupsRowClick: () -> Unit = {},
displayActionButton: Boolean = true
onBackupsRowClick: () -> Unit = {}
) {
Rows.TextRow(
modifier = Modifier.wrapContentHeight(),
@@ -497,18 +481,14 @@ private fun PendingBackupRow(
style = MaterialTheme.typography.bodyMedium
)
ViewSettingsButton(onBackupsRowClick, displayActionButton)
ViewSettingsButton(onBackupsRowClick)
}
}
)
}
@Composable
private fun ViewSettingsButton(onClick: () -> Unit, visible: Boolean = true) {
if (!visible) {
return
}
private fun ViewSettingsButton(onClick: () -> Unit) {
Buttons.MediumTonal(
onClick = onClick,
modifier = Modifier.padding(top = 12.dp)
@@ -523,8 +503,7 @@ private fun ViewSettingsButton(onClick: () -> Unit, visible: Boolean = true) {
private fun LocalStoreBackupRow(
backupState: BackupState.LocalStore,
lastBackupAt: Duration,
onBackupsRowClick: () -> Unit,
displayActionButton: Boolean = true
onBackupsRowClick: () -> Unit
) {
Rows.TextRow(
modifier = Modifier.wrapContentHeight(),
@@ -559,7 +538,7 @@ private fun LocalStoreBackupRow(
)
LastBackedUpText(lastBackupAt)
ViewSettingsButton(onBackupsRowClick, displayActionButton)
ViewSettingsButton(onBackupsRowClick)
}
}
)
@@ -569,8 +548,7 @@ private fun LocalStoreBackupRow(
private fun ActiveBackupsRow(
backupState: BackupState.WithTypeAndRenewalTime,
lastBackupAt: Duration,
onBackupsRowClick: () -> Unit = {},
displayActionButton: Boolean = true
onBackupsRowClick: () -> Unit = {}
) {
Rows.TextRow(
modifier = Modifier.wrapContentHeight(),
@@ -637,7 +615,7 @@ private fun ActiveBackupsRow(
LastBackedUpText(lastBackupAt)
ViewSettingsButton(onBackupsRowClick, displayActionButton)
ViewSettingsButton(onBackupsRowClick)
}
}
)
@@ -473,7 +473,11 @@ private fun RemoteBackupsSettingsContent(
}
if (state.isLinkedDevice) {
appendReducedBackupDetailsItems(state)
appendReducedBackupDetailsItems(
state = state,
backupRestoreState = backupRestoreState,
contentCallbacks = contentCallbacks
)
} else if (backupDeleteState != DeletionState.NONE && backupDeleteState != DeletionState.CLEAR_LOCAL_STATE) {
appendBackupDeletionItems(
backupDeleteState = backupDeleteState,
@@ -969,7 +973,9 @@ private fun LazyListScope.appendBackupDetailsItems(
}
private fun LazyListScope.appendReducedBackupDetailsItems(
state: RemoteBackupsSettingsState
state: RemoteBackupsSettingsState,
backupRestoreState: BackupRestoreState,
contentCallbacks: ContentCallbacks
) {
item {
Dividers.Default()
@@ -979,6 +985,14 @@ private fun LazyListScope.appendReducedBackupDetailsItems(
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
}
if (backupRestoreState is BackupRestoreState.Restoring) {
appendRestoreFromBackupStatusData(
backupRestoreState = backupRestoreState,
canRestoreUsingCellular = state.canRestoreUsingCellular,
contentCallbacks = contentCallbacks
)
}
item {
ReadOnlyLastBackupRow(lastBackupTimestamp = state.lastBackupTimestamp)
}
@@ -153,31 +153,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
}
viewModelScope.launch(Dispatchers.Default) {
var optimizedRemainingBytes = 0L
while (isActive) {
if (ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }) {
Log.d(TAG, "Backup is being restored. Collecting updates.")
ArchiveRestoreProgress
.stateFlow
.takeWhile { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
.onEach { latest -> _restoreState.update { BackupRestoreState.Restoring(latest) } }
.collect()
} 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 {
_restoreState.update { BackupRestoreState.None }
}
delay(1.seconds)
}
}
observeRestoreState()
viewModelScope.launch {
var previous: ArchiveUploadProgressState.State? = null
@@ -230,6 +206,36 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
viewModelScope.launch(Dispatchers.IO) {
BackupRepository.refreshBackupFileTimestamp()
}
observeRestoreState()
}
private fun observeRestoreState() {
viewModelScope.launch(Dispatchers.Default) {
var optimizedRemainingBytes = 0L
while (isActive) {
if (ArchiveRestoreProgress.state.let { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }) {
Log.d(TAG, "Backup is being restored. Collecting updates.")
ArchiveRestoreProgress
.stateFlow
.takeWhile { it.restoreState.isMediaRestoreOperation || it.restoreStatus == RestoreStatus.FINISHED }
.onEach { latest -> _restoreState.update { BackupRestoreState.Restoring(latest) } }
.collect()
} 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 {
_restoreState.update { BackupRestoreState.None }
}
delay(1.seconds)
}
}
}
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {