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

@@ -0,0 +1,17 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup
/**
* EventBus event for backup creation progress. Each subclass identifies the backup destination,
* allowing subscribers to receive only the events they care about via the @Subscribe method
* parameter type.
*/
sealed class BackupCreationEvent(val progress: BackupCreationProgress) {
class RemoteEncrypted(progress: BackupCreationProgress) : BackupCreationEvent(progress)
class LocalEncrypted(progress: BackupCreationProgress) : BackupCreationEvent(progress)
class LocalPlaintext(progress: BackupCreationProgress) : BackupCreationEvent(progress)
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup
/**
* Unified progress model for backup creation, shared across all backup destinations
* (remote encrypted, local encrypted, local plaintext).
*
* The export phase is identical regardless of destination — the same data is serialized.
* The transfer phase differs: remote uploads to CDN, local writes to disk.
*/
sealed interface BackupCreationProgress {
data object Idle : BackupCreationProgress
data object Canceled : BackupCreationProgress
/**
* The backup is being exported from the database into a serialized format.
*/
data class Exporting(
val phase: ExportPhase,
val frameExportCount: Long = 0,
val frameTotalCount: Long = 0
) : BackupCreationProgress
/**
* Post-export phase: the backup file and/or media are being transferred to their destination.
* For remote backups this means uploading; for local backups this means writing to disk.
*
* [completed] and [total] are unitless — they may represent bytes (remote upload) or
* item counts (local attachment export). The ratio [completed]/[total] yields progress.
*/
data class Transferring(
val completed: Long,
val total: Long,
val mediaPhase: Boolean
) : BackupCreationProgress
enum class ExportPhase {
NONE,
ACCOUNT,
RECIPIENT,
THREAD,
CALL,
STICKER,
NOTIFICATION_PROFILE,
CHAT_FOLDER,
MESSAGE
}
fun exportProgress(): Float {
return when (this) {
is Exporting -> if (frameTotalCount == 0L) 0f else frameExportCount / frameTotalCount.toFloat()
else -> 0f
}
}
fun transferProgress(): Float {
return when (this) {
is Transferring -> if (total == 0L) 0f else completed / total.toFloat()
else -> 0f
}
}
}

View File

@@ -1,21 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
class LocalBackupV2Event(val type: Type, val count: Long = 0, val estimatedTotalCount: Long = 0) {
enum class Type {
PROGRESS_ACCOUNT,
PROGRESS_RECIPIENT,
PROGRESS_THREAD,
PROGRESS_CALL,
PROGRESS_STICKER,
NOTIFICATION_PROFILE,
CHAT_FOLDER,
PROGRESS_MESSAGE,
PROGRESS_ATTACHMENT,
FINISHED
}
}

View File

@@ -15,8 +15,9 @@ import org.signal.core.util.Util
import org.signal.core.util.logging.Log
import org.signal.core.util.readFully
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.BackupCreationEvent
import org.thoughtcrime.securesms.backup.BackupCreationProgress
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.LocalBackupV2Event
import org.thoughtcrime.securesms.backup.v2.local.proto.FilesFrame
import org.thoughtcrime.securesms.backup.v2.local.proto.Metadata
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -233,45 +234,54 @@ object LocalArchiver {
private var lastVerboseUpdate: Long = 0
override fun onAccount() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_ACCOUNT))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.ACCOUNT))
}
override fun onRecipient() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_RECIPIENT))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.RECIPIENT))
}
override fun onThread() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_THREAD))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.THREAD))
}
override fun onCall() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_CALL))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.CALL))
}
override fun onSticker() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_STICKER))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.STICKER))
}
override fun onNotificationProfile() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.NOTIFICATION_PROFILE))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.NOTIFICATION_PROFILE))
}
override fun onChatFolder() {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.CHAT_FOLDER))
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.CHAT_FOLDER))
}
override fun onMessage(currentProgress: Long, approximateCount: Long) {
if (lastVerboseUpdate > System.currentTimeMillis() || lastVerboseUpdate + 1000 < System.currentTimeMillis() || currentProgress >= approximateCount) {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_MESSAGE, currentProgress, approximateCount))
lastVerboseUpdate = System.currentTimeMillis()
}
if (shouldThrottle(currentProgress >= approximateCount)) return
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.MESSAGE, frameExportCount = currentProgress, frameTotalCount = approximateCount))
}
override fun onAttachment(currentProgress: Long, totalCount: Long) {
if (lastVerboseUpdate > System.currentTimeMillis() || lastVerboseUpdate + 1000 < System.currentTimeMillis() || currentProgress >= totalCount) {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.PROGRESS_ATTACHMENT, currentProgress, totalCount))
lastVerboseUpdate = System.currentTimeMillis()
if (shouldThrottle(currentProgress >= totalCount)) return
post(BackupCreationProgress.Transferring(completed = currentProgress, total = totalCount, mediaPhase = true))
}
private fun shouldThrottle(forceUpdate: Boolean): Boolean {
val now = System.currentTimeMillis()
if (forceUpdate || lastVerboseUpdate > now || lastVerboseUpdate + 1000 < now) {
lastVerboseUpdate = now
return false
}
return true
}
private fun post(progress: BackupCreationProgress) {
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(progress))
}
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui.status
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.BackupCreationProgress
import org.signal.core.ui.R as CoreUiR
@Composable
fun BackupCreationProgressRow(
progress: BackupCreationProgress,
isRemote: Boolean,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.padding(horizontal = dimensionResource(id = CoreUiR.dimen.gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
BackupCreationProgressIndicator(progress = progress)
Text(
text = getProgressMessage(progress, isRemote),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
@Composable
private fun BackupCreationProgressIndicator(
progress: BackupCreationProgress
) {
val fraction = when (progress) {
is BackupCreationProgress.Exporting -> progress.exportProgress()
is BackupCreationProgress.Transferring -> progress.transferProgress()
else -> 0f
}
val hasDeterminateProgress = when (progress) {
is BackupCreationProgress.Exporting -> progress.frameTotalCount > 0 && progress.phase == BackupCreationProgress.ExportPhase.MESSAGE
is BackupCreationProgress.Transferring -> progress.total > 0
else -> false
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
if (hasDeterminateProgress) {
val animatedProgress by animateFloatAsState(targetValue = fraction, animationSpec = tween(durationMillis = 250))
LinearProgressIndicator(
trackColor = MaterialTheme.colorScheme.secondaryContainer,
progress = { animatedProgress },
drawStopIndicator = {},
modifier = Modifier
.weight(1f)
.padding(vertical = 12.dp)
)
} else {
LinearProgressIndicator(
trackColor = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier
.weight(1f)
.padding(vertical = 12.dp)
)
}
}
}
@Composable
private fun getProgressMessage(progress: BackupCreationProgress, isRemote: Boolean): String {
return when (progress) {
is BackupCreationProgress.Exporting -> getExportPhaseMessage(progress)
is BackupCreationProgress.Transferring -> getTransferPhaseMessage(progress, isRemote)
else -> stringResource(R.string.BackupCreationProgressRow__processing_backup)
}
}
@Composable
private fun getExportPhaseMessage(progress: BackupCreationProgress.Exporting): String {
return when (progress.phase) {
BackupCreationProgress.ExportPhase.MESSAGE -> {
if (progress.frameTotalCount > 0) {
stringResource(
R.string.BackupCreationProgressRow__processing_messages_s_of_s_d,
"%,d".format(progress.frameExportCount),
"%,d".format(progress.frameTotalCount),
(progress.exportProgress() * 100).toInt()
)
} else {
stringResource(R.string.BackupCreationProgressRow__processing_messages)
}
}
BackupCreationProgress.ExportPhase.NONE -> stringResource(R.string.BackupCreationProgressRow__processing_backup)
else -> stringResource(R.string.BackupCreationProgressRow__preparing_backup)
}
}
@Composable
private fun getTransferPhaseMessage(progress: BackupCreationProgress.Transferring, isRemote: Boolean): String {
val percent = (progress.transferProgress() * 100).toInt()
return if (isRemote) {
stringResource(R.string.BackupCreationProgressRow__uploading_media_d, percent)
} else {
stringResource(R.string.BackupCreationProgressRow__exporting_media_d, percent)
}
}
@DayNightPreviews
@Composable
private fun ExportingIndeterminatePreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.NONE),
isRemote = false
)
}
}
@DayNightPreviews
@Composable
private fun ExportingMessagesPreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Exporting(
phase = BackupCreationProgress.ExportPhase.MESSAGE,
frameExportCount = 1000,
frameTotalCount = 100_000
),
isRemote = false
)
}
}
@DayNightPreviews
@Composable
private fun TransferringLocalPreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
),
isRemote = false
)
}
}
@DayNightPreviews
@Composable
private fun TransferringRemotePreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
),
isRemote = true
)
}
}

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?) {

View File

@@ -7,13 +7,15 @@ import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.Stopwatch
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.BackupFileIOError
import org.thoughtcrime.securesms.backup.FullBackupExporter.BackupCanceledException
import org.thoughtcrime.securesms.backup.v2.LocalBackupV2Event
import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.NotificationChannels
@@ -106,14 +108,14 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
snapshotFileSystem.finalize()
stopwatch.split("archive-finalize")
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.FINISHED))
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(BackupCreationProgress.Idle))
} catch (e: BackupCanceledException) {
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.FINISHED))
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(BackupCreationProgress.Idle))
Log.w(TAG, "Archive cancelled")
throw e
} catch (e: IOException) {
Log.w(TAG, "Error during archive!", e)
EventBus.getDefault().post(LocalBackupV2Event(LocalBackupV2Event.Type.FINISHED))
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(BackupCreationProgress.Idle))
BackupFileIOError.postNotificationForException(context, e)
throw e
} finally {
@@ -148,23 +150,49 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
private class ProgressUpdater {
var notification: NotificationController? = null
private var previousType: LocalBackupV2Event.Type? = null
private var previousPhase: NotificationPhase? = null
@Subscribe(threadMode = ThreadMode.POSTING)
fun onEvent(event: LocalBackupV2Event) {
fun onEvent(event: BackupCreationEvent.LocalEncrypted) {
val notification = notification ?: return
val progress = event.progress
if (previousType != event.type) {
notification.replaceTitle(event.type.toString()) // todo [local-backup] use actual strings
previousType = event.type
}
when (progress) {
is BackupCreationProgress.Exporting -> {
val phase = NotificationPhase.Export(progress.phase)
if (previousPhase != phase) {
notification.replaceTitle(progress.phase.toString())
previousPhase = phase
}
if (progress.frameTotalCount == 0L) {
notification.setIndeterminateProgress()
} else {
notification.setProgress(progress.frameTotalCount, progress.frameExportCount)
}
}
if (event.estimatedTotalCount == 0L) {
notification.setIndeterminateProgress()
} else {
notification.setProgress(event.estimatedTotalCount, event.count)
is BackupCreationProgress.Transferring -> {
if (previousPhase !is NotificationPhase.Transfer) {
notification.replaceTitle(AppDependencies.application.getString(R.string.LocalArchiveJob__exporting_media))
previousPhase = NotificationPhase.Transfer
}
if (progress.total == 0L) {
notification.setIndeterminateProgress()
} else {
notification.setProgress(progress.total, progress.completed)
}
}
else -> {
notification.setIndeterminateProgress()
}
}
}
private sealed interface NotificationPhase {
data class Export(val phase: BackupCreationProgress.ExportPhase) : NotificationPhase
data object Transfer : NotificationPhase
}
}
class Factory : Job.Factory<LocalArchiveJob?> {

View File

@@ -8744,6 +8744,24 @@
<!-- Button label to open QR scanner to scan a device and transfer a backup -->
<string name="RemoteBackupsSettingsFragment__scan_qr_code">Scan QR code</string>
<!-- BackupCreationProgressRow -->
<!-- Progress text shown while the backup database is being processed -->
<string name="BackupCreationProgressRow__processing_backup">Processing backup…</string>
<!-- Progress text shown while the backup is being prepared (e.g. exporting accounts, recipients, etc.) -->
<string name="BackupCreationProgressRow__preparing_backup">Preparing backup…</string>
<!-- Progress text shown while messages are being processed, with no count available yet -->
<string name="BackupCreationProgressRow__processing_messages">Processing messages…</string>
<!-- Progress text shown while messages are being processed. %1$s is the formatted current count (e.g. "1,000"), %2$s is the formatted total count (e.g. "100,000"), %3$d is the percent complete (e.g. 42). -->
<string name="BackupCreationProgressRow__processing_messages_s_of_s_d">Processing messages: %1$s of %2$s (%3$d%%)</string>
<!-- Progress text shown while media is being uploaded to the server. %1$d is the percent complete (e.g. 42). -->
<string name="BackupCreationProgressRow__uploading_media_d">Uploading media: %1$d%%</string>
<!-- Progress text shown while media is being exported to local storage. %1$d is the percent complete (e.g. 42). -->
<string name="BackupCreationProgressRow__exporting_media_d">Exporting media: %1$d%%</string>
<!-- LocalArchiveJob -->
<!-- Notification title shown while media attachments are being exported during a local backup -->
<string name="LocalArchiveJob__exporting_media">Exporting media…</string>
<!-- OnDeviceBackupsImprovementsScreen -->
<!-- Title displayed at the top of the screen explaining on-device backup improvements -->
<string name="OnDeviceBackupsImprovementsScreen__improvements_to_on_device_backups">Improvements to on-device backups</string>