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.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
}

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 {
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,

View File

@@ -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?) {

View File

@@ -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__")
}

View File

@@ -49,8 +49,12 @@ class ArchiveAttachmentBackfillJob private constructor(parameters: Parameters) :
ArchiveUploadProgress.onAttachmentsStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
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()
}

View File

@@ -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 {

View File

@@ -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)
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)

View File

@@ -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)
if (!isCanceled) {
AppDependencies.jobManager.add(CopyAttachmentToArchiveJob(attachment.attachmentId))
} else {
Log.d(TAG, "[$attachmentId] Job was canceled. Skipping copy job.")
}
return Result.success()
}

View File

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