From 09e47dba3a4c997f2690b58b77f85d49668fb2d1 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 20 May 2025 15:46:24 -0300 Subject: [PATCH] Add support for cancelling an in-progress archive upload. Co-authored-by: Jeffrey Starke --- .../securesms/backup/ArchiveUploadProgress.kt | 36 +++++++++++++-- .../remote/RemoteBackupsSettingsFragment.kt | 45 ++++++------------- .../remote/RemoteBackupsSettingsViewModel.kt | 2 +- .../InternalBackupPlaygroundViewModel.kt | 8 ++-- .../jobs/ArchiveAttachmentBackfillJob.kt | 8 +++- .../securesms/jobs/BackupMessagesJob.kt | 2 +- .../jobs/CopyAttachmentToArchiveJob.kt | 12 ++++- .../jobs/UploadAttachmentToArchiveJob.kt | 16 +++++-- app/src/main/protowire/KeyValue.proto | 1 + 9 files changed, 82 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt index 344f2e7c0f..f097425336 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt @@ -16,6 +16,10 @@ import org.signal.core.util.throttleLatest import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.ArchiveThumbnailUploadJob +import org.thoughtcrime.securesms.jobs.BackfillDigestJob +import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import java.util.concurrent.ConcurrentHashMap @@ -85,13 +89,27 @@ object ArchiveUploadProgress { get() = uploadProgress.state != ArchiveUploadProgressState.State.None fun begin() { - updateState { + updateState(overrideCancel = true) { ArchiveUploadProgressState( state = ArchiveUploadProgressState.State.Export ) } } + fun cancel() { + updateState { + ArchiveUploadProgressState( + state = ArchiveUploadProgressState.State.UserCanceled + ) + } + + AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE) + UploadAttachmentToArchiveJob.getAllQueueKeys().forEach { + AppDependencies.jobManager.cancelAllInQueue(it) + } + AppDependencies.jobManager.cancelAllInQueue(ArchiveThumbnailUploadJob.KEY) + } + fun onMessageBackupCreated(backupFileSize: Long) { updateState { it.copy( @@ -144,8 +162,20 @@ object ArchiveUploadProgress { updateState { PROGRESS_NONE } } - private fun updateState(notify: Boolean = true, transform: (ArchiveUploadProgressState) -> ArchiveUploadProgressState) { - val newState = transform(uploadProgress) + private fun updateState( + notify: Boolean = true, + overrideCancel: Boolean = false, + transform: (ArchiveUploadProgressState) -> ArchiveUploadProgressState + ) { + val newState = transform(uploadProgress).let { state -> + val oldArchiveState = uploadProgress.state + if (oldArchiveState == ArchiveUploadProgressState.State.UserCanceled && !overrideCancel) { + state.copy(state = ArchiveUploadProgressState.State.UserCanceled) + } else { + state + } + } + if (uploadProgress == newState) { return } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 48f09a8bad..4839803598 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -639,7 +639,7 @@ private fun LazyListScope.appendBackupDetailsItems( } } - if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None) { + if (backupProgress == null || backupProgress.state == ArchiveUploadProgressState.State.None || backupProgress.state == ArchiveUploadProgressState.State.UserCanceled) { item { LastBackupRow( lastBackupTimestamp = lastBackupTimestamp, @@ -1074,16 +1074,19 @@ private fun InProgressBackupRow( .padding(top = 16.dp, bottom = 14.dp) ) { Column( - modifier = Modifier.weight(1f), - verticalArrangement = spacedBy(12.dp) + modifier = Modifier.weight(1f) ) { when (archiveUploadProgressState.state) { - ArchiveUploadProgressState.State.None -> { + ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.UserCanceled -> { ArchiveProgressIndicator() } ArchiveUploadProgressState.State.Export -> { val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.frameExportProgress(), animationSpec = tween(durationMillis = 250)) - ArchiveProgressIndicator(progress = { progressValue }) + ArchiveProgressIndicator( + progress = { progressValue }, + isCancelable = true, + cancel = cancelArchiveUpload + ) } ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> { val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.uploadProgress(), animationSpec = tween(durationMillis = 250)) @@ -1110,12 +1113,14 @@ private fun ArchiveProgressIndicator( isCancelable: Boolean = false, cancel: () -> Unit = {} ) { - Row { + Row( + verticalAlignment = Alignment.CenterVertically + ) { LinearProgressIndicator( trackColor = MaterialTheme.colorScheme.secondaryContainer, progress = progress, drawStopIndicator = {}, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.weight(1f).padding(vertical = 12.dp) ) if (isCancelable) { @@ -1132,7 +1137,7 @@ private fun ArchiveProgressIndicator( @Composable private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String { return when (archiveUploadProgressState.state) { - ArchiveUploadProgressState.State.None -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup) + ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.UserCanceled -> stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup) ArchiveUploadProgressState.State.Export -> getBackupExportPhaseProgressString(archiveUploadProgressState) ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState) } @@ -1628,30 +1633,6 @@ private fun InProgressRowPreview() { backupPhase = ArchiveUploadProgressState.BackupPhase.Account ) ) - InProgressBackupRow( - archiveUploadProgressState = ArchiveUploadProgressState( - state = ArchiveUploadProgressState.State.Export, - backupPhase = ArchiveUploadProgressState.BackupPhase.Call - ) - ) - InProgressBackupRow( - archiveUploadProgressState = ArchiveUploadProgressState( - state = ArchiveUploadProgressState.State.Export, - backupPhase = ArchiveUploadProgressState.BackupPhase.Sticker - ) - ) - InProgressBackupRow( - archiveUploadProgressState = ArchiveUploadProgressState( - state = ArchiveUploadProgressState.State.Export, - backupPhase = ArchiveUploadProgressState.BackupPhase.Recipient - ) - ) - InProgressBackupRow( - archiveUploadProgressState = ArchiveUploadProgressState( - state = ArchiveUploadProgressState.State.Export, - backupPhase = ArchiveUploadProgressState.BackupPhase.Thread - ) - ) InProgressBackupRow( archiveUploadProgressState = ArchiveUploadProgressState( state = ArchiveUploadProgressState.State.Export, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 53813766d8..0f9588f2f5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -214,7 +214,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { } fun cancelUpload() { - // TODO [message-backups] -- Perform cancel of media upload. + ArchiveUploadProgress.cancel() } private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index eb8904b36e..3a9251d030 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -31,6 +31,7 @@ import org.signal.core.util.stream.LimitedInputStream import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.backup.ArchiveUploadProgress import org.thoughtcrime.securesms.backup.v2.ArchiveValidator import org.thoughtcrime.securesms.backup.v2.BackupMetadata import org.thoughtcrime.securesms.backup.v2.BackupRepository @@ -46,7 +47,6 @@ import org.thoughtcrime.securesms.database.MessageType import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.AttachmentUploadJob -import org.thoughtcrime.securesms.jobs.BackfillDigestJob import org.thoughtcrime.securesms.jobs.BackupMessagesJob import org.thoughtcrime.securesms.jobs.BackupRestoreJob import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob @@ -218,10 +218,8 @@ class InternalBackupPlaygroundViewModel : ViewModel() { } fun haltAllJobs() { - AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE) - AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_0") - AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1") - AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob") + ArchiveUploadProgress.cancel() + AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob") AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentBackfillJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentBackfillJob.kt index be370815c9..3d89fd3187 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentBackfillJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveAttachmentBackfillJob.kt @@ -49,8 +49,12 @@ class ArchiveAttachmentBackfillJob private constructor(parameters: Parameters) : ArchiveUploadProgress.onAttachmentsStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes()) - Log.i(TAG, "Adding ${jobs.size} jobs to backfill attachments.") - AppDependencies.jobManager.addAll(jobs) + if (!isCanceled) { + Log.i(TAG, "Adding ${jobs.size} jobs to backfill attachments.") + AppDependencies.jobManager.addAll(jobs) + } else { + Log.w(TAG, "Job was canceled. Not enqueuing backfill.") + } return Result.success() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index a37385a440..c901eab9cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -186,7 +186,7 @@ class BackupMessagesJob private constructor( stopwatch.split("used-space") stopwatch.stop(TAG) - if (SignalStore.backup.backsUpMedia && SignalDatabase.attachments.doAnyAttachmentsNeedArchiveUpload()) { + if (SignalStore.backup.backsUpMedia && SignalDatabase.attachments.doAnyAttachmentsNeedArchiveUpload() && !isCanceled) { Log.i(TAG, "Enqueuing attachment backfill job.") AppDependencies.jobManager.add(ArchiveAttachmentBackfillJob()) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt index 6adc120d27..fa5cdd40c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CopyAttachmentToArchiveJob.kt @@ -86,6 +86,11 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A return Result.failure() } + if (isCanceled) { + Log.w(TAG, "[$attachmentId] Canceled. Refusing to proceed.") + return Result.failure() + } + if (attachment.archiveTransferState == AttachmentTable.ArchiveTransferState.NONE) { Log.i(TAG, "[$attachmentId] Not marked as pending copy. Enqueueing an upload job instead.") AppDependencies.jobManager.add(UploadAttachmentToArchiveJob(attachmentId)) @@ -138,7 +143,12 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A Log.d(TAG, "[$attachmentId] Updating archive transfer state to ${AttachmentTable.ArchiveTransferState.FINISHED}") SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.FINISHED) - ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId) + if (!isCanceled) { + ArchiveThumbnailUploadJob.enqueueIfNecessary(attachmentId) + } else { + Log.d(TAG, "[$attachmentId] Refusing to enqueue thumb for canceled upload.") + } + SignalStore.backup.usedBackupMediaSpace += AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size)) ArchiveUploadProgress.onAttachmentFinished(attachmentId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt index 524e122f7e..17fa4642dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/UploadAttachmentToArchiveJob.kt @@ -30,6 +30,7 @@ import java.io.FileNotFoundException import java.io.IOException import java.net.ProtocolException import kotlin.random.Random +import kotlin.random.nextInt import kotlin.time.Duration.Companion.days /** @@ -45,12 +46,17 @@ class UploadAttachmentToArchiveJob private constructor( companion object { private val TAG = Log.tag(UploadAttachmentToArchiveJob::class) const val KEY = "UploadAttachmentToArchiveJob" + const val MAX_JOB_QUEUES = 2 /** - * This randomly selects between one of two queues. It's a fun way of limiting the concurrency of the upload jobs to + * This randomly selects between one of [MAX_JOB_QUEUES] queues. It's a fun way of limiting the concurrency of the upload jobs to * take up at most two job runners. */ - fun buildQueueKey() = "ArchiveAttachmentJobs_${Random.nextInt(0, 2)}" + fun buildQueueKey( + queue: Int = Random.nextInt(0, MAX_JOB_QUEUES) + ) = "ArchiveAttachmentJobs_$queue" + + fun getAllQueueKeys() = (0 until MAX_JOB_QUEUES).map { buildQueueKey(queue = it) } } constructor(attachmentId: AttachmentId) : this( @@ -182,7 +188,11 @@ class UploadAttachmentToArchiveJob private constructor( SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachment.attachmentId, uploadResult) - AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachment.attachmentId)) + if (!isCanceled) { + AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachment.attachmentId)) + } else { + Log.d(TAG, "[$attachmentId] Job was canceled. Skipping copy job.") + } return Result.success() } diff --git a/app/src/main/protowire/KeyValue.proto b/app/src/main/protowire/KeyValue.proto index c7f61160bf..790b468c3b 100644 --- a/app/src/main/protowire/KeyValue.proto +++ b/app/src/main/protowire/KeyValue.proto @@ -22,6 +22,7 @@ message ArchiveUploadProgressState { Export = 1; UploadBackupFile = 2; UploadMedia = 3; + UserCanceled = 4; } /**