mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 08:23:00 +01:00
Unify backup creation progress model for local backups.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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?> {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user