Add support for cancelling an in-progress archive upload.

Co-authored-by: Jeffrey Starke <jeffrey@signal.org>
This commit is contained in:
Alex Hart
2025-05-20 15:46:24 -03:00
committed by GitHub
parent 3751052697
commit 09e47dba3a
9 changed files with 82 additions and 48 deletions

View File

@@ -16,6 +16,10 @@ import org.signal.core.util.throttleLatest
import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.database.SignalDatabase 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.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -85,13 +89,27 @@ object ArchiveUploadProgress {
get() = uploadProgress.state != ArchiveUploadProgressState.State.None get() = uploadProgress.state != ArchiveUploadProgressState.State.None
fun begin() { fun begin() {
updateState { updateState(overrideCancel = true) {
ArchiveUploadProgressState( ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.Export 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) { fun onMessageBackupCreated(backupFileSize: Long) {
updateState { updateState {
it.copy( it.copy(
@@ -144,8 +162,20 @@ object ArchiveUploadProgress {
updateState { PROGRESS_NONE } updateState { PROGRESS_NONE }
} }
private fun updateState(notify: Boolean = true, transform: (ArchiveUploadProgressState) -> ArchiveUploadProgressState) { private fun updateState(
val newState = transform(uploadProgress) 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) { if (uploadProgress == newState) {
return return
} }

View File

@@ -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 { item {
LastBackupRow( LastBackupRow(
lastBackupTimestamp = lastBackupTimestamp, lastBackupTimestamp = lastBackupTimestamp,
@@ -1074,16 +1074,19 @@ private fun InProgressBackupRow(
.padding(top = 16.dp, bottom = 14.dp) .padding(top = 16.dp, bottom = 14.dp)
) { ) {
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f)
verticalArrangement = spacedBy(12.dp)
) { ) {
when (archiveUploadProgressState.state) { when (archiveUploadProgressState.state) {
ArchiveUploadProgressState.State.None -> { ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.UserCanceled -> {
ArchiveProgressIndicator() ArchiveProgressIndicator()
} }
ArchiveUploadProgressState.State.Export -> { ArchiveUploadProgressState.State.Export -> {
val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.frameExportProgress(), animationSpec = tween(durationMillis = 250)) 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 -> { ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> {
val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.uploadProgress(), animationSpec = tween(durationMillis = 250)) val progressValue by animateFloatAsState(targetValue = archiveUploadProgressState.uploadProgress(), animationSpec = tween(durationMillis = 250))
@@ -1110,12 +1113,14 @@ private fun ArchiveProgressIndicator(
isCancelable: Boolean = false, isCancelable: Boolean = false,
cancel: () -> Unit = {} cancel: () -> Unit = {}
) { ) {
Row { Row(
verticalAlignment = Alignment.CenterVertically
) {
LinearProgressIndicator( LinearProgressIndicator(
trackColor = MaterialTheme.colorScheme.secondaryContainer, trackColor = MaterialTheme.colorScheme.secondaryContainer,
progress = progress, progress = progress,
drawStopIndicator = {}, drawStopIndicator = {},
modifier = Modifier.fillMaxWidth() modifier = Modifier.weight(1f).padding(vertical = 12.dp)
) )
if (isCancelable) { if (isCancelable) {
@@ -1132,7 +1137,7 @@ private fun ArchiveProgressIndicator(
@Composable @Composable
private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String { private fun getProgressStateMessage(archiveUploadProgressState: ArchiveUploadProgressState): String {
return when (archiveUploadProgressState.state) { 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.Export -> getBackupExportPhaseProgressString(archiveUploadProgressState)
ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState) ArchiveUploadProgressState.State.UploadBackupFile, ArchiveUploadProgressState.State.UploadMedia -> getBackupUploadPhaseProgressString(archiveUploadProgressState)
} }
@@ -1628,30 +1633,6 @@ private fun InProgressRowPreview() {
backupPhase = ArchiveUploadProgressState.BackupPhase.Account 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( InProgressBackupRow(
archiveUploadProgressState = ArchiveUploadProgressState( archiveUploadProgressState = ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.Export, state = ArchiveUploadProgressState.State.Export,

View File

@@ -214,7 +214,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
} }
fun cancelUpload() { fun cancelUpload() {
// TODO [message-backups] -- Perform cancel of media upload. ArchiveUploadProgress.cancel()
} }
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) { private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {

View File

@@ -31,6 +31,7 @@ import org.signal.core.util.stream.LimitedInputStream
import org.signal.libsignal.zkgroup.profiles.ProfileKey import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment 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.ArchiveValidator
import org.thoughtcrime.securesms.backup.v2.BackupMetadata import org.thoughtcrime.securesms.backup.v2.BackupMetadata
import org.thoughtcrime.securesms.backup.v2.BackupRepository 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.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.BackfillDigestJob
import org.thoughtcrime.securesms.jobs.BackupMessagesJob import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreJob import org.thoughtcrime.securesms.jobs.BackupRestoreJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
@@ -218,10 +218,8 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
} }
fun haltAllJobs() { fun haltAllJobs() {
AppDependencies.jobManager.cancelAllInQueue(BackfillDigestJob.QUEUE) ArchiveUploadProgress.cancel()
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_0")
AppDependencies.jobManager.cancelAllInQueue("ArchiveAttachmentJobs_1")
AppDependencies.jobManager.cancelAllInQueue("ArchiveThumbnailUploadJob")
AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob") AppDependencies.jobManager.cancelAllInQueue("BackupRestoreJob")
AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__") AppDependencies.jobManager.cancelAllInQueue("__LOCAL_BACKUP__")
} }

View File

@@ -49,8 +49,12 @@ class ArchiveAttachmentBackfillJob private constructor(parameters: Parameters) :
ArchiveUploadProgress.onAttachmentsStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes()) ArchiveUploadProgress.onAttachmentsStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
Log.i(TAG, "Adding ${jobs.size} jobs to backfill attachments.") if (!isCanceled) {
AppDependencies.jobManager.addAll(jobs) 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() return Result.success()
} }

View File

@@ -186,7 +186,7 @@ class BackupMessagesJob private constructor(
stopwatch.split("used-space") stopwatch.split("used-space")
stopwatch.stop(TAG) 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.") Log.i(TAG, "Enqueuing attachment backfill job.")
AppDependencies.jobManager.add(ArchiveAttachmentBackfillJob()) AppDependencies.jobManager.add(ArchiveAttachmentBackfillJob())
} else { } else {

View File

@@ -86,6 +86,11 @@ class CopyAttachmentToArchiveJob private constructor(private val attachmentId: A
return Result.failure() return Result.failure()
} }
if (isCanceled) {
Log.w(TAG, "[$attachmentId] Canceled. Refusing to proceed.")
return Result.failure()
}
if (attachment.archiveTransferState == AttachmentTable.ArchiveTransferState.NONE) { if (attachment.archiveTransferState == AttachmentTable.ArchiveTransferState.NONE) {
Log.i(TAG, "[$attachmentId] Not marked as pending copy. Enqueueing an upload job instead.") Log.i(TAG, "[$attachmentId] Not marked as pending copy. Enqueueing an upload job instead.")
AppDependencies.jobManager.add(UploadAttachmentToArchiveJob(attachmentId)) 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}") Log.d(TAG, "[$attachmentId] Updating archive transfer state to ${AttachmentTable.ArchiveTransferState.FINISHED}")
SignalDatabase.attachments.setArchiveTransferState(attachmentId, 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)) SignalStore.backup.usedBackupMediaSpace += AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(attachment.size))
ArchiveUploadProgress.onAttachmentFinished(attachmentId) ArchiveUploadProgress.onAttachmentFinished(attachmentId)

View File

@@ -30,6 +30,7 @@ import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.net.ProtocolException import java.net.ProtocolException
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
/** /**
@@ -45,12 +46,17 @@ class UploadAttachmentToArchiveJob private constructor(
companion object { companion object {
private val TAG = Log.tag(UploadAttachmentToArchiveJob::class) private val TAG = Log.tag(UploadAttachmentToArchiveJob::class)
const val KEY = "UploadAttachmentToArchiveJob" 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. * 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( constructor(attachmentId: AttachmentId) : this(
@@ -182,7 +188,11 @@ class UploadAttachmentToArchiveJob private constructor(
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachment.attachmentId, uploadResult) 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() return Result.success()
} }

View File

@@ -22,6 +22,7 @@ message ArchiveUploadProgressState {
Export = 1; Export = 1;
UploadBackupFile = 2; UploadBackupFile = 2;
UploadMedia = 3; UploadMedia = 3;
UserCanceled = 4;
} }
/** /**