Add progress phases for initialization and finalization for local backups.

This commit is contained in:
Alex Hart
2026-03-16 14:50:02 -03:00
committed by Michelle Tang
parent d2c8b6e14c
commit 2f41d15a41
14 changed files with 254 additions and 213 deletions

View File

@@ -214,6 +214,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
.addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addNonBlocking(BackupRepository::maybeFixAnyDanglingUploadProgress)
.addNonBlocking(BackupRepository::maybeFixAnyDanglingLocalExportProgress)
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)

View File

@@ -1,17 +0,0 @@
/*
* 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

@@ -5,62 +5,17 @@
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
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
/**
* 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
val LocalBackupCreationProgress.isIdle: Boolean
get() = idle != null || (exporting == null && transferring == null && canceled == null)
/**
* 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
}
}
fun LocalBackupCreationProgress.exportProgress(): Float {
val exporting = exporting ?: return 0f
return if (exporting.frameTotalCount == 0L) 0f else exporting.frameExportCount / exporting.frameTotalCount.toFloat()
}
fun LocalBackupCreationProgress.transferProgress(): Float {
val transferring = transferring ?: return 0f
return if (transferring.total == 0L) 0f else transferring.completed / transferring.total.toFloat()
}

View File

@@ -63,6 +63,7 @@ import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.isIdle
import org.thoughtcrime.securesms.backup.v2.BackupRepository.copyAttachmentToArchive
import org.thoughtcrime.securesms.backup.v2.BackupRepository.exportForDebugging
import org.thoughtcrime.securesms.backup.v2.importer.ChatItemArchiveImporter
@@ -113,6 +114,7 @@ import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.CancelRestoreMediaJob
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
import org.thoughtcrime.securesms.jobs.LocalArchiveJob
import org.thoughtcrime.securesms.jobs.LocalBackupJob
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
@@ -129,6 +131,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.isDecisionPending
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.notifications.NotificationChannels
@@ -588,6 +591,14 @@ object BackupRepository {
SignalStore.backup.snoozeDownloadNotifier()
}
@JvmStatic
fun maybeFixAnyDanglingLocalExportProgress() {
if (!SignalStore.backup.newLocalBackupProgress.isIdle && AppDependencies.jobManager.find { it.factoryKey == LocalArchiveJob.KEY }.isEmpty()) {
Log.w(TAG, "Found stale local backup progress with no active job. Resetting to idle.")
SignalStore.backup.newLocalBackupProgress = LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle())
}
}
@JvmStatic
fun maybeFixAnyDanglingUploadProgress() {
if (SignalStore.account.isLinkedDevice) {

View File

@@ -161,10 +161,10 @@ class ArchiveFileSystem private constructor(private val context: Context, root:
* Clean up unused files in the shared files directory leveraged across all current snapshots. A file
* is unused if it is not referenced directly by any current snapshots.
*/
fun deleteUnusedFiles() {
fun deleteUnusedFiles(allFilesProgressListener: AllFilesProgressListener? = null) {
Log.i(TAG, "Deleting unused files")
val allFiles: MutableMap<String, DocumentFileInfo> = filesFileSystem.allFiles().toMutableMap()
val allFiles: MutableMap<String, DocumentFileInfo> = filesFileSystem.allFiles(allFilesProgressListener).toMutableMap()
val snapshots: List<SnapshotInfo> = listSnapshots()
snapshots
@@ -268,14 +268,17 @@ class FilesFileSystem(private val context: Context, private val root: DocumentFi
/**
* Enumerate all files in the directory.
*/
fun allFiles(): Map<String, DocumentFileInfo> {
fun allFiles(allFilesProgressListener: AllFilesProgressListener? = null): Map<String, DocumentFileInfo> {
val allFiles = HashMap<String, DocumentFileInfo>()
val total = subFolders.values.size
for (subfolder in subFolders.values) {
subFolders.values.forEachIndexed { index, subfolder ->
val subFiles = subfolder.listFiles(context)
for (file in subFiles) {
allFiles[file.name] = file
}
allFilesProgressListener?.onProgress(index + 1, total)
}
return allFiles
@@ -330,3 +333,7 @@ private fun String.toMilliseconds(): Long {
return -1
}
fun interface AllFilesProgressListener {
fun onProgress(completed: Int, total: Int)
}

View File

@@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.backup.v2.local
import okio.ByteString.Companion.toByteString
import org.greenrobot.eventbus.EventBus
import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MediaName
import org.signal.core.util.Stopwatch
@@ -15,13 +14,12 @@ 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.local.proto.FilesFrame
import org.thoughtcrime.securesms.backup.v2.local.proto.Metadata
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
@@ -67,8 +65,11 @@ object LocalArchiver {
mainStream = snapshotFileSystem.mainOutputStream() ?: return ArchiveResult.failure(ArchiveFailure.MainStream)
Log.i(TAG, "Listing all current files")
val allFiles = filesFileSystem.allFiles()
val allFiles = filesFileSystem.allFiles { completed, total ->
SignalStore.backup.newLocalBackupProgress = LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.INITIALIZING, frameExportCount = completed.toLong(), frameTotalCount = total.toLong()))
}
stopwatch.split("files-list")
SignalStore.backup.newLocalBackupProgress = LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.INITIALIZING))
val mediaNames: MutableSet<MediaName> = Collections.synchronizedSet(HashSet())
@@ -234,41 +235,41 @@ object LocalArchiver {
private var lastVerboseUpdate: Long = 0
override fun onAccount() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.ACCOUNT))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.ACCOUNT)))
}
override fun onRecipient() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.RECIPIENT))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.RECIPIENT)))
}
override fun onThread() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.THREAD))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.THREAD)))
}
override fun onCall() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.CALL))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.CALL)))
}
override fun onSticker() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.STICKER))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.STICKER)))
}
override fun onNotificationProfile() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.NOTIFICATION_PROFILE))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.NOTIFICATION_PROFILE)))
}
override fun onChatFolder() {
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.CHAT_FOLDER))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.CHAT_FOLDER)))
}
override fun onMessage(currentProgress: Long, approximateCount: Long) {
if (shouldThrottle(currentProgress >= approximateCount)) return
post(BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.MESSAGE, frameExportCount = currentProgress, frameTotalCount = approximateCount))
post(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.MESSAGE, frameExportCount = currentProgress, frameTotalCount = approximateCount)))
}
override fun onAttachment(currentProgress: Long, totalCount: Long) {
if (shouldThrottle(currentProgress >= totalCount)) return
post(BackupCreationProgress.Transferring(completed = currentProgress, total = totalCount, mediaPhase = true))
post(LocalBackupCreationProgress(transferring = LocalBackupCreationProgress.Transferring(completed = currentProgress, total = totalCount, mediaPhase = true)))
}
private fun shouldThrottle(forceUpdate: Boolean): Boolean {
@@ -280,8 +281,8 @@ object LocalArchiver {
return true
}
private fun post(progress: BackupCreationProgress) {
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(progress))
private fun post(progress: LocalBackupCreationProgress) {
SignalStore.backup.newLocalBackupProgress = progress
}
}
}

View File

@@ -23,12 +23,14 @@ 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.thoughtcrime.securesms.backup.exportProgress
import org.thoughtcrime.securesms.backup.transferProgress
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.signal.core.ui.R as CoreUiR
@Composable
fun BackupCreationProgressRow(
progress: BackupCreationProgress,
progress: LocalBackupCreationProgress,
isRemote: Boolean,
modifier: Modifier = Modifier
) {
@@ -53,17 +55,20 @@ fun BackupCreationProgressRow(
@Composable
private fun BackupCreationProgressIndicator(
progress: BackupCreationProgress
progress: LocalBackupCreationProgress
) {
val fraction = when (progress) {
is BackupCreationProgress.Exporting -> progress.exportProgress()
is BackupCreationProgress.Transferring -> progress.transferProgress()
val exporting = progress.exporting
val transferring = progress.transferring
val fraction = when {
exporting != null -> progress.exportProgress()
transferring != null -> 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
val hasDeterminateProgress = when {
exporting != null -> exporting.frameTotalCount > 0 && (exporting.phase == LocalBackupCreationProgress.ExportPhase.MESSAGE || exporting.phase == LocalBackupCreationProgress.ExportPhase.INITIALIZING || exporting.phase == LocalBackupCreationProgress.ExportPhase.FINALIZING)
transferring != null -> transferring.total > 0
else -> false
}
@@ -92,37 +97,41 @@ private fun BackupCreationProgressIndicator(
}
@Composable
private fun getProgressMessage(progress: BackupCreationProgress, isRemote: Boolean): String {
return when (progress) {
is BackupCreationProgress.Exporting -> getExportPhaseMessage(progress)
is BackupCreationProgress.Transferring -> getTransferPhaseMessage(progress, isRemote)
private fun getProgressMessage(progress: LocalBackupCreationProgress, isRemote: Boolean): String {
val exporting = progress.exporting
val transferring = progress.transferring
return when {
exporting != null -> getExportPhaseMessage(exporting, progress)
transferring != null -> getTransferPhaseMessage(transferring, 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) {
private fun getExportPhaseMessage(exporting: LocalBackupCreationProgress.Exporting, progress: LocalBackupCreationProgress): String {
return when (exporting.phase) {
LocalBackupCreationProgress.ExportPhase.MESSAGE -> {
if (exporting.frameTotalCount > 0) {
stringResource(
R.string.BackupCreationProgressRow__processing_messages_s_of_s_d,
"%,d".format(progress.frameExportCount),
"%,d".format(progress.frameTotalCount),
"%,d".format(exporting.frameExportCount),
"%,d".format(exporting.frameTotalCount),
(progress.exportProgress() * 100).toInt()
)
} else {
stringResource(R.string.BackupCreationProgressRow__processing_messages)
}
}
BackupCreationProgress.ExportPhase.NONE -> stringResource(R.string.BackupCreationProgressRow__processing_backup)
LocalBackupCreationProgress.ExportPhase.NONE -> stringResource(R.string.BackupCreationProgressRow__processing_backup)
LocalBackupCreationProgress.ExportPhase.FINALIZING -> stringResource(R.string.BackupCreationProgressRow__finalizing)
else -> stringResource(R.string.BackupCreationProgressRow__preparing_backup)
}
}
@Composable
private fun getTransferPhaseMessage(progress: BackupCreationProgress.Transferring, isRemote: Boolean): String {
val percent = (progress.transferProgress() * 100).toInt()
private fun getTransferPhaseMessage(transferring: LocalBackupCreationProgress.Transferring, isRemote: Boolean): String {
val percent = if (transferring.total == 0L) 0 else (transferring.completed * 100 / transferring.total).toInt()
return if (isRemote) {
stringResource(R.string.BackupCreationProgressRow__uploading_media_d, percent)
} else {
@@ -135,7 +144,35 @@ private fun getTransferPhaseMessage(progress: BackupCreationProgress.Transferrin
private fun ExportingIndeterminatePreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.NONE),
progress = LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.NONE)),
isRemote = false
)
}
}
@DayNightPreviews
@Composable
private fun InitializingIndeterminatePreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.INITIALIZING)),
isRemote = false
)
}
}
@DayNightPreviews
@Composable
private fun InitializingDeterminatePreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = LocalBackupCreationProgress(
exporting = LocalBackupCreationProgress.Exporting(
phase = LocalBackupCreationProgress.ExportPhase.INITIALIZING,
frameExportCount = 128,
frameTotalCount = 256
)
),
isRemote = false
)
}
@@ -146,10 +183,12 @@ private fun ExportingIndeterminatePreview() {
private fun ExportingMessagesPreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Exporting(
phase = BackupCreationProgress.ExportPhase.MESSAGE,
frameExportCount = 1000,
frameTotalCount = 100_000
progress = LocalBackupCreationProgress(
exporting = LocalBackupCreationProgress.Exporting(
phase = LocalBackupCreationProgress.ExportPhase.MESSAGE,
frameExportCount = 1000,
frameTotalCount = 100_000
)
),
isRemote = false
)
@@ -161,10 +200,12 @@ private fun ExportingMessagesPreview() {
private fun TransferringLocalPreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
progress = LocalBackupCreationProgress(
transferring = LocalBackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
)
),
isRemote = false
)
@@ -176,10 +217,12 @@ private fun TransferringLocalPreview() {
private fun TransferringRemotePreview() {
Previews.Preview {
BackupCreationProgressRow(
progress = BackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
progress = LocalBackupCreationProgress(
transferring = LocalBackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
)
),
isRemote = true
)

View File

@@ -36,9 +36,10 @@ 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.isIdle
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupCreationProgressRow
import org.thoughtcrime.securesms.components.compose.rememberBiometricsAuthentication
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.util.BackupUtil
import org.signal.core.ui.R as CoreUiR
import org.signal.core.ui.compose.DayNightPreviews as DayNightPreview
@@ -120,7 +121,7 @@ internal fun LocalBackupsSettingsScreen(
)
}
} else {
val isCreating = state.progress !is BackupCreationProgress.Idle
val isCreating = !state.progress.isIdle
if (isCreating) {
item {
@@ -272,7 +273,7 @@ private fun LocalBackupsSettingsEnabledIdlePreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupCreationProgress.Idle
progress = LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle())
),
callback = LocalBackupsSettingsCallback.Empty
)
@@ -289,8 +290,8 @@ private fun LocalBackupsSettingsEnabledExportingIndeterminatePreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupCreationProgress.Exporting(
phase = BackupCreationProgress.ExportPhase.ACCOUNT
progress = LocalBackupCreationProgress(
exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.ACCOUNT)
)
),
callback = LocalBackupsSettingsCallback.Empty
@@ -308,10 +309,12 @@ private fun LocalBackupsSettingsEnabledExportingMessagesPreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "/storage/emulated/0/Signal/Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupCreationProgress.Exporting(
phase = BackupCreationProgress.ExportPhase.MESSAGE,
frameExportCount = 42000,
frameTotalCount = 100000
progress = LocalBackupCreationProgress(
exporting = LocalBackupCreationProgress.Exporting(
phase = LocalBackupCreationProgress.ExportPhase.MESSAGE,
frameExportCount = 42000,
frameTotalCount = 100000
)
)
),
callback = LocalBackupsSettingsCallback.Empty
@@ -329,10 +332,12 @@ private fun LocalBackupsSettingsEnabledTransferringPreview() {
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
progress = LocalBackupCreationProgress(
transferring = LocalBackupCreationProgress.Transferring(
completed = 50,
total = 200,
mediaPhase = true
)
)
),
callback = LocalBackupsSettingsCallback.Empty
@@ -350,7 +355,7 @@ private fun LocalBackupsSettingsEnabledNonLegacyPreview() {
lastBackupLabel = "Last backup: 1 hour ago",
folderDisplayName = "Signal Backups",
scheduleTimeLabel = "1:00 AM",
progress = BackupCreationProgress.Idle
progress = LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle())
),
callback = LocalBackupsSettingsCallback.Empty
)

View File

@@ -4,7 +4,7 @@
*/
package org.thoughtcrime.securesms.components.settings.app.backups.local
import org.thoughtcrime.securesms.backup.BackupCreationProgress
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
/**
* Immutable state for the on-device backups settings screen.
@@ -18,6 +18,6 @@ data class LocalBackupsSettingsState(
val lastBackupLabel: String? = null,
val folderDisplayName: String? = null,
val scheduleTimeLabel: String? = null,
val progress: BackupCreationProgress = BackupCreationProgress.Idle,
val progress: LocalBackupCreationProgress = LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle()),
val isDeleting: Boolean = false
)

View File

@@ -14,20 +14,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
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.components.settings.app.backups.remote.BackupKeyCredentialManagerHandler
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.LocalBackupJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.util.BackupUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.TextSecurePreferences
@@ -77,11 +73,11 @@ class LocalBackupsViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
}
}
EventBus.getDefault().register(this)
}
override fun onCleared() {
EventBus.getDefault().unregister(this)
viewModelScope.launch {
SignalStore.backup.newLocalBackupProgressFlow.collect { progress ->
internalSettingsState.update { it.copy(progress = progress) }
}
}
}
fun refreshSettingsState() {
@@ -112,14 +108,7 @@ class LocalBackupsViewModel : ViewModel(), BackupKeyCredentialManagerHandler {
}
fun onBackupStarted() {
internalSettingsState.update {
it.copy(progress = BackupCreationProgress.Exporting(phase = BackupCreationProgress.ExportPhase.NONE))
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onBackupEvent(event: BackupCreationEvent.LocalEncrypted) {
internalSettingsState.update { it.copy(progress = event.progress) }
SignalStore.backup.newLocalBackupProgress = LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.NONE))
}
fun turnOffAndDelete(context: Context) {

View File

@@ -1,14 +1,9 @@
package org.thoughtcrime.securesms.jobs
import android.net.Uri
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
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.local.ArchiveFileSystem
@@ -18,6 +13,7 @@ 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.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.service.GenericForegroundService
import org.thoughtcrime.securesms.service.NotificationController
@@ -49,8 +45,6 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
BackupFileIOError.clearNotification(context)
val updater = ProgressUpdater()
var notification: NotificationController? = null
try {
notification = GenericForegroundService.startForegroundTask(
@@ -64,9 +58,8 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
}
try {
updater.notification = notification
EventBus.getDefault().register(updater)
notification?.setIndeterminateProgress()
setProgress(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.INITIALIZING)), notification)
val stopwatch = Stopwatch("archive-export")
@@ -108,14 +101,14 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
snapshotFileSystem.finalize()
stopwatch.split("archive-finalize")
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(BackupCreationProgress.Idle))
setProgress(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.FINALIZING)), notification)
} catch (e: BackupCanceledException) {
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(BackupCreationProgress.Idle))
setProgress(LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle()), notification)
Log.w(TAG, "Archive cancelled")
throw e
} catch (e: IOException) {
Log.w(TAG, "Error during archive!", e)
EventBus.getDefault().post(BackupCreationEvent.LocalEncrypted(BackupCreationProgress.Idle))
setProgress(LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle()), notification)
BackupFileIOError.postNotificationForException(context, e)
throw e
} finally {
@@ -129,72 +122,76 @@ class LocalArchiveJob internal constructor(parameters: Parameters) : Job(paramet
archiveFileSystem.deleteOldBackups()
stopwatch.split("delete-old")
archiveFileSystem.deleteUnusedFiles()
archiveFileSystem.deleteUnusedFiles { completed, total ->
setProgress(LocalBackupCreationProgress(exporting = LocalBackupCreationProgress.Exporting(phase = LocalBackupCreationProgress.ExportPhase.FINALIZING, frameExportCount = completed.toLong(), frameTotalCount = total.toLong())), notification)
}
stopwatch.split("delete-unused")
stopwatch.stop(TAG)
setProgress(LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle()), notification)
SignalStore.backup.newLocalBackupsLastBackupTime = System.currentTimeMillis()
} finally {
notification?.close()
EventBus.getDefault().unregister(updater)
updater.notification = null
}
return Result.success()
}
override fun onFailure() {
SignalStore.backup.newLocalBackupProgress = LocalBackupCreationProgress(idle = LocalBackupCreationProgress.Idle())
}
private class ProgressUpdater {
var notification: NotificationController? = null
private fun setProgress(progress: LocalBackupCreationProgress, notification: NotificationController?) {
SignalStore.backup.newLocalBackupProgress = progress
updateNotification(progress, notification)
}
private var previousPhase: NotificationPhase? = null
private var previousPhase: NotificationPhase? = null
@Subscribe(threadMode = ThreadMode.POSTING)
fun onEvent(event: BackupCreationEvent.LocalEncrypted) {
val notification = notification ?: return
val progress = event.progress
private fun updateNotification(progress: LocalBackupCreationProgress, notification: NotificationController?) {
if (notification == null) return
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)
}
val exporting = progress.exporting
val transferring = progress.transferring
when {
exporting != null -> {
val phase = NotificationPhase.Export(exporting.phase)
if (previousPhase != phase) {
notification.replaceTitle(exporting.phase.toString())
previousPhase = phase
}
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 -> {
if (exporting.frameTotalCount == 0L) {
notification.setIndeterminateProgress()
} else {
notification.setProgress(exporting.frameTotalCount, exporting.frameExportCount)
}
}
}
private sealed interface NotificationPhase {
data class Export(val phase: BackupCreationProgress.ExportPhase) : NotificationPhase
data object Transfer : NotificationPhase
transferring != null -> {
if (previousPhase !is NotificationPhase.Transfer) {
notification.replaceTitle(AppDependencies.application.getString(R.string.LocalArchiveJob__exporting_media))
previousPhase = NotificationPhase.Transfer
}
if (transferring.total == 0L) {
notification.setIndeterminateProgress()
} else {
notification.setProgress(transferring.total, transferring.completed)
}
}
else -> {
notification.setIndeterminateProgress()
}
}
}
private sealed interface NotificationPhase {
data class Export(val phase: LocalBackupCreationProgress.ExportPhase) : NotificationPhase
data object Transfer : NotificationPhase
}
class Factory : Job.Factory<LocalArchiveJob?> {
override fun create(parameters: Parameters, serializedData: ByteArray?): LocalArchiveJob {
return LocalArchiveJob(parameters)

View File

@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollecti
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.keyvalue.protos.BackupDownloadNotifierState
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.util.Environment
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
@@ -104,6 +105,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_NEW_LOCAL_BACKUPS_DIRECTORY = "backup.new_local_backups_directory"
private const val KEY_NEW_LOCAL_BACKUPS_LAST_BACKUP_TIME = "backup.new_local_backups_last_backup_time"
private const val KEY_NEW_LOCAL_BACKUPS_SELECTED_SNAPSHOT_TIMESTAMP = "backup.new_local_backups_selected_snapshot_timestamp"
private const val KEY_NEW_LOCAL_BACKUPS_CREATION_PROGRESS = "backup.new_local_backups_creation_progress"
private const val KEY_UPLOAD_BANNER_VISIBLE = "backup.upload_banner_visible"
@@ -474,6 +476,13 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
val newLocalBackupsEnabledFlow: Flow<Boolean> by lazy { newLocalBackupsEnabledValue.toFlow() }
/**
* Progress values for local backup progress.
*/
private val newLocalBackupProgressValue = protoValue(KEY_NEW_LOCAL_BACKUPS_CREATION_PROGRESS, LocalBackupCreationProgress(), LocalBackupCreationProgress.ADAPTER)
var newLocalBackupProgress: LocalBackupCreationProgress by newLocalBackupProgressValue
val newLocalBackupProgressFlow: Flow<LocalBackupCreationProgress> by lazy { newLocalBackupProgressValue.toFlow() }
/**IT
* The directory URI path selected for new local backups.
*/
private val newLocalBackupsDirectoryValue = stringValue(KEY_NEW_LOCAL_BACKUPS_DIRECTORY, null as String?)