mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-18 15:56:24 +01:00
Add progress phases for initialization and finalization for local backups.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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?)
|
||||
|
||||
Reference in New Issue
Block a user