diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index dd86b5f4ca..95d641bffc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationEvent.kt deleted file mode 100644 index b5b7b16240..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationEvent.kt +++ /dev/null @@ -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) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationProgress.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationProgress.kt index f6a30c8881..2bd1d16511 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationProgress.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupCreationProgress.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 08e9ac506a..c41ed4e97e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt index 49e7adc746..ae502235d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt @@ -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 = filesFileSystem.allFiles().toMutableMap() + val allFiles: MutableMap = filesFileSystem.allFiles(allFilesProgressListener).toMutableMap() val snapshots: List = 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 { + fun allFiles(allFilesProgressListener: AllFilesProgressListener? = null): Map { val allFiles = HashMap() + 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) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt index f52771e7cc..ed218736ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/LocalArchiver.kt @@ -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 = 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 } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreationProgressRow.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreationProgressRow.kt index 6d2ec6d4a2..740c440a2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreationProgressRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreationProgressRow.kt @@ -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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsScreen.kt index ce3fb82626..276e2b25a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsScreen.kt @@ -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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsState.kt index 6268a24bc7..04fd8c647c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsSettingsState.kt @@ -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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt index 75a2bc9b57..5e1da84b25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/local/LocalBackupsViewModel.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalArchiveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalArchiveJob.kt index 12c3114644..af07d3a5c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalArchiveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/LocalArchiveJob.kt @@ -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 { override fun create(parameters: Parameters, serializedData: ByteArray?): LocalArchiveJob { return LocalArchiveJob(parameters) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index d087a64ab4..a209ec2d0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -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 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 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?) diff --git a/app/src/main/protowire/KeyValue.proto b/app/src/main/protowire/KeyValue.proto index e754345cf6..19a297d628 100644 --- a/app/src/main/protowire/KeyValue.proto +++ b/app/src/main/protowire/KeyValue.proto @@ -51,6 +51,44 @@ message ArchiveUploadProgressState { uint64 mediaTotalBytes = 8; } +message LocalBackupCreationProgress { + message Idle {} + message Canceled {} + + message Exporting { + ExportPhase phase = 1; + uint64 frameExportCount = 2; + uint64 frameTotalCount = 3; + } + + message Transferring { + uint64 completed = 1; + uint64 total = 2; + bool mediaPhase = 3; + } + + enum ExportPhase { + NONE = 0; + INITIALIZING = 1; + ACCOUNT = 2; + RECIPIENT = 3; + THREAD = 4; + CALL = 5; + STICKER = 6; + NOTIFICATION_PROFILE = 7; + CHAT_FOLDER = 8; + MESSAGE = 9; + FINALIZING = 10; + } + + oneof state { + Idle idle = 1; + Canceled canceled = 2; + Exporting exporting = 3; + Transferring transferring = 4; + } +} + message BackupDownloadNotifierState { enum Type { SHEET = 0; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bf9f1697a..f419998dd5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8754,6 +8754,8 @@ Processing backup… Preparing backup… + + Finalizing… Processing messages…