Unify backup creation progress model for local backups.

This commit is contained in:
Alex Hart
2026-03-12 13:25:51 -03:00
committed by Michelle Tang
parent cc282276c8
commit db17d1fd24
11 changed files with 418 additions and 181 deletions

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.local
/**
* Progress indicator state for the on-device backups creation/verification workflow.
*/
sealed class BackupProgressState {
data object Idle : BackupProgressState()
/**
* Represents either backup creation or verification progress.
*
* @param summary High-level status label (e.g. "In progress…", "Verifying backup…")
* @param percentLabel Secondary progress label (either a percent string or a count-based string)
* @param progressFraction Optional progress fraction in \\([0, 1]\\). Null indicates indeterminate progress.
*/
data class InProgress(
val summary: String,
val percentLabel: String,
val progressFraction: Float?
) : BackupProgressState()
}

View File

@@ -5,10 +5,8 @@
package org.thoughtcrime.securesms.components.settings.app.backups.local
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
@@ -38,6 +36,8 @@ import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.Snackbars
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.BackupCreationProgress
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreationProgressRow
import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication
import org.thoughtcrime.securesms.util.BackupUtil
import org.signal.core.ui.R as CoreUiR
@@ -120,51 +120,26 @@ internal fun LocalBackupsSettingsScreen(
)
}
} else {
val isCreating = state.progress is BackupProgressState.InProgress
val isCreating = state.progress !is BackupCreationProgress.Idle
item {
Rows.TextRow(
text = {
Column {
Text(
text = stringResource(id = R.string.BackupsPreferenceFragment__create_backup),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
if (state.progress is BackupProgressState.InProgress) {
if (isCreating) {
item {
BackupCreationProgressRow(
progress = state.progress,
isRemote = false
)
}
} else {
item {
Rows.TextRow(
text = {
Column {
Text(
text = state.progress.summary,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp)
text = stringResource(id = R.string.BackupsPreferenceFragment__create_backup),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
if (state.progress.progressFraction == null) {
LinearProgressIndicator(
trackColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
)
} else {
LinearProgressIndicator(
trackColor = MaterialTheme.colorScheme.secondaryContainer,
progress = { state.progress.progressFraction },
drawStopIndicator = {},
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp)
)
}
Text(
text = state.progress.percentLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 8.dp)
)
} else {
Text(
text = state.lastBackupLabel.orEmpty(),
style = MaterialTheme.typography.bodyMedium,
@@ -172,11 +147,10 @@ internal fun LocalBackupsSettingsScreen(
modifier = Modifier.padding(top = 4.dp)
)
}
}
},
enabled = !isCreating,
onClick = callback::onCreateBackupClick
)
},
onClick = callback::onCreateBackupClick
)
}
}
item {
@@ -224,7 +198,7 @@ internal fun LocalBackupsSettingsScreen(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(
horizontal = dimensionResource(id = org.signal.core.ui.R.dimen.gutter),
horizontal = dimensionResource(id = CoreUiR.dimen.gutter),
vertical = 16.dp
)
)
@@ -294,7 +268,7 @@ private fun LocalBackupsSettingsEnabledIdlePreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupProgressState.Idle
progress = BackupCreationProgress.Idle
),
callback = LocalBackupsSettingsCallback.Empty
)
@@ -303,7 +277,7 @@ private fun LocalBackupsSettingsEnabledIdlePreview() {
@DayNightPreview
@Composable
private fun LocalBackupsSettingsEnabledInProgressIndeterminatePreview() {
private fun LocalBackupsSettingsEnabledExportingIndeterminatePreview() {
Previews.Preview {
LocalBackupsSettingsScreen(
state = LocalBackupsSettingsState(
@@ -311,10 +285,8 @@ private fun LocalBackupsSettingsEnabledInProgressIndeterminatePreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupProgressState.InProgress(
summary = "In progress…",
percentLabel = "123 so far…",
progressFraction = null
progress = BackupCreationProgress.Exporting(
phase = BackupCreationProgress.ExportPhase.ACCOUNT
)
),
callback = LocalBackupsSettingsCallback.Empty
@@ -324,7 +296,7 @@ private fun LocalBackupsSettingsEnabledInProgressIndeterminatePreview() {
@DayNightPreview
@Composable
private fun LocalBackupsSettingsEnabledInProgressPercentPreview() {
private fun LocalBackupsSettingsEnabledExportingMessagesPreview() {
Previews.Preview {
LocalBackupsSettingsScreen(
state = LocalBackupsSettingsState(
@@ -332,10 +304,31 @@ private fun LocalBackupsSettingsEnabledInProgressPercentPreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupProgressState.InProgress(
summary = "In progress…",
percentLabel = "42.0% so far…",
progressFraction = 0.42f
progress = BackupCreationProgress.Exporting(
phase = BackupCreationProgress.ExportPhase.MESSAGE,
frameExportCount = 42000,
frameTotalCount = 100000
)
),
callback = LocalBackupsSettingsCallback.Empty
)
}
}
@DayNightPreview
@Composable
private fun LocalBackupsSettingsEnabledTransferringPreview() {
Previews.Preview {
LocalBackupsSettingsScreen(
state = LocalBackupsSettingsState(
backupsEnabled = true,
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
)
),
callback = LocalBackupsSettingsCallback.Empty
@@ -353,7 +346,7 @@ private fun LocalBackupsSettingsEnabledNonLegacyPreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "Signal Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupProgressState.Idle
progress = BackupCreationProgress.Idle
),
callback = LocalBackupsSettingsCallback.Empty
)

View File

@@ -4,8 +4,10 @@
*/
package org.thoughtcrime.securesms.components.settings.app.backups.local
import org.thoughtcrime.securesms.backup.BackupCreationProgress
/**
* Immutable state for the on-device (legacy) backups settings screen.
* Immutable state for the on-device backups settings screen.
*
* This is intended to be the single source of truth for UI rendering (i.e. a single `StateFlow`
* emission fully describes what the screen should display).
@@ -16,5 +18,5 @@ data class LocalBackupsSettingsState(
val lastBackupLabel: String? = null,
val folderDisplayName: String? = null,
val scheduleTimeLabel: String? = null,
val progress: BackupProgressState = BackupProgressState.Idle
val progress: BackupCreationProgress = BackupCreationProgress.Idle
)

View File

@@ -20,8 +20,9 @@ import org.greenrobot.eventbus.ThreadMode
import org.signal.core.ui.util.StorageUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.BackupCreationEvent
import org.thoughtcrime.securesms.backup.BackupCreationProgress
import org.thoughtcrime.securesms.backup.BackupPassphrase
import org.thoughtcrime.securesms.backup.v2.LocalBackupV2Event
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
import org.thoughtcrime.securesms.dependencies.AppDependencies
@@ -31,7 +32,6 @@ import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.formatHours
import java.text.NumberFormat
import java.time.LocalTime
import java.util.Locale
@@ -44,11 +44,6 @@ class LocalBackupsViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
private val TAG = Log.tag(LocalBackupsViewModel::class)
}
private val formatter: NumberFormat = NumberFormat.getInstance().apply {
minimumFractionDigits = 1
maximumFractionDigits = 1
}
private val internalSettingsState = MutableStateFlow(
LocalBackupsSettingsState(
backupsEnabled = SignalStore.backup.newLocalBackupsEnabled,
@@ -117,46 +112,14 @@ class LocalBackupsViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
}
fun onBackupStarted() {
val context = AppDependencies.application
internalSettingsState.update {
it.copy(
progress = BackupProgressState.InProgress(
summary = context.getString(R.string.BackupsPreferenceFragment__in_progress),
percentLabel = context.getString(R.string.BackupsPreferenceFragment__d_so_far, 0),
progressFraction = null
)
)
it.copy(progress = BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.NONE))
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onBackupEvent(event: LocalBackupV2Event) {
val context = AppDependencies.application
when (event.type) {
LocalBackupV2Event.Type.FINISHED -> {
internalSettingsState.update { it.copy(progress = BackupProgressState.Idle) }
}
else -> {
val summary = context.getString(R.string.BackupsPreferenceFragment__in_progress)
val progressState = if (event.estimatedTotalCount == 0L) {
BackupProgressState.InProgress(
summary = summary,
percentLabel = context.getString(R.string.BackupsPreferenceFragment__d_so_far, event.count),
progressFraction = null
)
} else {
val fraction = ((event.count / event.estimatedTotalCount.toDouble()) / 100.0).toFloat().coerceIn(0f, 1f)
BackupProgressState.InProgress(
summary = summary,
percentLabel = context.getString(R.string.BackupsPreferenceFragment__s_so_far, formatter.format((event.count / event.estimatedTotalCount.toDouble()))),
progressFraction = fraction
)
}
internalSettingsState.update { it.copy(progress = progressState) }
}
}
fun onBackupEvent(event: BackupCreationEvent.LocalEncrypted) {
internalSettingsState.update { it.copy(progress = event.progress) }
}
override fun updateBackupKeySaveState(newState: BackupKeySaveState?) {