Fix backups deletion pipeline.

This commit is contained in:
Alex Hart
2025-07-25 10:30:47 -03:00
committed by Michelle Tang
parent 3bb2ab3a0c
commit 53ee0648c0
11 changed files with 160 additions and 67 deletions

View File

@@ -35,6 +35,11 @@ enum class DeletionState(private val id: Int) {
*/ */
AWAITING_MEDIA_DOWNLOAD(1), AWAITING_MEDIA_DOWNLOAD(1),
/**
* Media has downloaded so the deletion job can pick up from where it left off.
*/
MEDIA_DOWNLOAD_FINISHED(5),
/** /**
* Deleting the backups themselves. * Deleting the backups themselves.
* User should see the "deleting backups..." UX * User should see the "deleting backups..." UX
@@ -47,6 +52,12 @@ enum class DeletionState(private val id: Int) {
*/ */
COMPLETE(3); COMPLETE(3);
fun isInProgress(): Boolean {
return this != FAILED && this != NONE && this != COMPLETE
}
fun isIdle(): Boolean = !isInProgress()
companion object { companion object {
val serializer: LongSerializer<DeletionState> = Serializer() val serializer: LongSerializer<DeletionState> = Serializer()
} }
@@ -56,11 +67,12 @@ enum class DeletionState(private val id: Int) {
return data.id.toLong() return data.id.toLong()
} }
override fun deserialize(data: Long): DeletionState { override fun deserialize(input: Long): DeletionState {
return when (data.toInt()) { return when (input.toInt()) {
FAILED.id -> FAILED FAILED.id -> FAILED
CLEAR_LOCAL_STATE.id -> CLEAR_LOCAL_STATE CLEAR_LOCAL_STATE.id -> CLEAR_LOCAL_STATE
AWAITING_MEDIA_DOWNLOAD.id -> AWAITING_MEDIA_DOWNLOAD AWAITING_MEDIA_DOWNLOAD.id -> AWAITING_MEDIA_DOWNLOAD
MEDIA_DOWNLOAD_FINISHED.id -> MEDIA_DOWNLOAD_FINISHED
DELETE_BACKUPS.id -> DELETE_BACKUPS DELETE_BACKUPS.id -> DELETE_BACKUPS
COMPLETE.id -> COMPLETE COMPLETE.id -> COMPLETE
else -> NONE else -> NONE

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.rx3.asFlowable
import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Dialogs
import org.signal.core.util.concurrent.SignalDispatchers import org.signal.core.util.concurrent.SignalDispatchers
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowViewModel import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowViewModel
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsStage import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsStage
@@ -80,7 +79,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
viewLifecycleOwner.lifecycleScope.launch(SignalDispatchers.Main) { viewLifecycleOwner.lifecycleScope.launch(SignalDispatchers.Main) {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.deletionState.collectLatest { viewModel.deletionState.collectLatest {
if (it == DeletionState.DELETE_BACKUPS) { if (it.isInProgress()) {
Toast.makeText( Toast.makeText(
requireContext(), requireContext(),
R.string.MessageBackupsFlowFragment__a_backup_deletion_is_in_progress, R.string.MessageBackupsFlowFragment__a_backup_deletion_is_in_progress,

View File

@@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -69,7 +68,6 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.setFragmentResultListener import androidx.fragment.app.setFragmentResultListener
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@@ -487,7 +485,7 @@ private fun RemoteBackupsSettingsContent(
state = state.backupState, state = state.backupState,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription, onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
onRenewClick = contentCallbacks::onRenewLostSubscription, onRenewClick = contentCallbacks::onRenewLostSubscription,
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS isRenewEnabled = backupDeleteState.isIdle()
) )
} }
@@ -497,7 +495,7 @@ private fun RemoteBackupsSettingsContent(
BackupCard( BackupCard(
backupState = state.backupState, backupState = state.backupState,
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick, onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick,
buttonsEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS buttonsEnabled = backupDeleteState.isIdle()
) )
} }
@@ -506,7 +504,7 @@ private fun RemoteBackupsSettingsContent(
title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found), title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
onRenewClick = contentCallbacks::onRenewLostSubscription, onRenewClick = contentCallbacks::onRenewLostSubscription,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription, onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS isRenewEnabled = backupDeleteState.isIdle()
) )
} }
@@ -535,6 +533,7 @@ private fun RemoteBackupsSettingsContent(
canRestoreUsingCellular = state.canRestoreUsingCellular, canRestoreUsingCellular = state.canRestoreUsingCellular,
canBackUpNow = !state.isOutOfStorageSpace, canBackUpNow = !state.isOutOfStorageSpace,
includeDebuglog = state.includeDebuglog, includeDebuglog = state.includeDebuglog,
backupMediaDetails = state.backupMediaDetails,
contentCallbacks = contentCallbacks contentCallbacks = contentCallbacks
) )
} else { } else {
@@ -591,7 +590,7 @@ private fun RemoteBackupsSettingsContent(
} }
RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER -> { RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER -> {
CircularProgressDialog(onDismiss = contentCallbacks::onDialogDismissed) Dialogs.IndeterminateProgressDialog(onDismissRequest = { contentCallbacks.onDialogDismissed() })
} }
RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> { RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> {
@@ -764,13 +763,13 @@ private fun LazyListScope.appendBackupDeletionItems(
} else { } else {
item { item {
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.fillMaxWidth() modifier = Modifier.horizontalGutters().fillMaxWidth()
) )
} }
} }
} }
DeletionState.DELETE_BACKUPS -> { DeletionState.MEDIA_DOWNLOAD_FINISHED, DeletionState.DELETE_BACKUPS -> {
item { item {
DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data)) DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data))
} }
@@ -841,6 +840,7 @@ private fun LazyListScope.appendBackupDetailsItems(
canRestoreUsingCellular: Boolean, canRestoreUsingCellular: Boolean,
canBackUpNow: Boolean, canBackUpNow: Boolean,
includeDebuglog: Boolean?, includeDebuglog: Boolean?,
backupMediaDetails: RemoteBackupsSettingsState.BackupMediaDetails?,
contentCallbacks: ContentCallbacks contentCallbacks: ContentCallbacks
) { ) {
item { item {
@@ -851,6 +851,16 @@ private fun LazyListScope.appendBackupDetailsItems(
Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details)) Texts.SectionHeader(text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_details))
} }
if (backupMediaDetails != null) {
item {
Column(modifier = Modifier.horizontalGutters()) {
Text("[Internal Only] Backup Media Details")
Text("Awaiting Restore: ${backupMediaDetails.awaitingRestore.toUnitString()}")
Text("Offloaded: ${backupMediaDetails.offloaded.toUnitString()}")
}
}
}
if (backupRestoreState !is BackupRestoreState.None) { if (backupRestoreState !is BackupRestoreState.None) {
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) { if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
appendRestoreFromBackupStatusData( appendRestoreFromBackupStatusData(
@@ -1643,35 +1653,6 @@ private fun ResumeRestoreOverCellularDialog(
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CircularProgressDialog(
onDismiss: () -> Unit
) {
BasicAlertDialog(
onDismissRequest = onDismiss,
properties = DialogProperties(
dismissOnBackPress = false,
dismissOnClickOutside = false
)
) {
Surface(
shape = Dialogs.Defaults.shape,
color = Dialogs.Defaults.containerColor
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.aspectRatio(1f)
) {
CircularProgressIndicator(
modifier = Modifier
.size(48.dp)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun BackupFrequencyDialog( private fun BackupFrequencyDialog(
@@ -2122,16 +2103,6 @@ private fun SkipDownloadDialogPreview() {
} }
} }
@SignalPreview
@Composable
private fun CircularProgressDialogPreview() {
Previews.Preview {
CircularProgressDialog(
onDismiss = {}
)
}
}
@SignalPreview @SignalPreview
@Composable @Composable
private fun BackupFrequencyDialogPreview() { private fun BackupFrequencyDialogPreview() {

View File

@@ -5,6 +5,7 @@
package org.thoughtcrime.securesms.components.settings.app.backups.remote package org.thoughtcrime.securesms.components.settings.app.backups.remote
import org.signal.core.util.ByteSize
import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.backups.BackupState import org.thoughtcrime.securesms.components.settings.app.backups.BackupState
@@ -28,9 +29,15 @@ data class RemoteBackupsSettingsState(
val dialog: Dialog = Dialog.NONE, val dialog: Dialog = Dialog.NONE,
val snackbar: Snackbar = Snackbar.NONE, val snackbar: Snackbar = Snackbar.NONE,
val includeDebuglog: Boolean? = null, val includeDebuglog: Boolean? = null,
val canBackupMessagesJobRun: Boolean = false val canBackupMessagesJobRun: Boolean = false,
val backupMediaDetails: BackupMediaDetails? = null
) { ) {
data class BackupMediaDetails(
val awaitingRestore: ByteSize,
val offloaded: ByteSize
)
enum class Dialog { enum class Dialog {
NONE, NONE,
TURN_OFF_AND_DELETE_BACKUPS, TURN_OFF_AND_DELETE_BACKUPS,

View File

@@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.withContext
import org.signal.core.util.bytes import org.signal.core.util.bytes
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.mebiBytes import org.signal.core.util.mebiBytes
@@ -44,6 +45,7 @@ import org.thoughtcrime.securesms.jobs.BackupMessagesJob
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 org.thoughtcrime.securesms.service.MessageBackupListener import org.thoughtcrime.securesms.service.MessageBackupListener
import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
@@ -81,7 +83,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_state.update { it.copy(backupMediaSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize()) } refreshBackupMediaSizeState()
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@@ -105,7 +107,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
.attachmentUpdates() .attachmentUpdates()
.throttleLatest(5.seconds) .throttleLatest(5.seconds)
.collectLatest { .collectLatest {
_state.update { it.copy(backupMediaSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize()) } refreshBackupMediaSizeState()
} }
} }
@@ -209,10 +211,14 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
} }
fun turnOffAndDeleteBackups() { fun turnOffAndDeleteBackups() {
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER) viewModelScope.launch {
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.IO) {
BackupRepository.turnOffAndDisableBackups() BackupRepository.turnOffAndDisableBackups()
}
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
} }
} }
@@ -229,6 +235,20 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
_state.update { it.copy(includeDebuglog = includeDebuglog) } _state.update { it.copy(includeDebuglog = includeDebuglog) }
} }
private fun refreshBackupMediaSizeState() {
_state.update {
it.copy(
backupMediaSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize(),
backupMediaDetails = if (RemoteConfig.internalUser || Environment.IS_STAGING) {
RemoteBackupsSettingsState.BackupMediaDetails(
awaitingRestore = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes,
offloaded = SignalDatabase.attachments.getOptimizedMediaAttachmentSize().bytes
)
} else null
)
}
}
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) { private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
try { try {
Log.i(TAG, "Performing a state refresh.") Log.i(TAG, "Performing a state refresh.")

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobmanager.impl
import android.app.job.JobInfo
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.jobmanager.Constraint
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* When we are awaiting media download, we want to suppress the running of the
* deletion job such that once media *is* downloaded it can finish off deleting
* the backup.
*/
object DeletionNotAwaitingMediaDownloadConstraint : Constraint {
const val KEY = "DeletionNotAwaitingMediaDownloadConstraint"
override fun isMet(): Boolean {
return SignalStore.backup.deletionState != DeletionState.AWAITING_MEDIA_DOWNLOAD
}
override fun getFactoryKey(): String = KEY
override fun applyToJobInfo(jobInfoBuilder: JobInfo.Builder) = Unit
object Observer : ConstraintObserver {
val listeners: MutableSet<ConstraintObserver.Notifier> = mutableSetOf()
override fun register(notifier: ConstraintObserver.Notifier) {
listeners += notifier
}
fun notifyListeners() {
for (listener in listeners) {
listener.onConstraintMet(KEY)
}
}
}
class Factory : Constraint.Factory<DeletionNotAwaitingMediaDownloadConstraint> {
override fun create(): DeletionNotAwaitingMediaDownloadConstraint {
return DeletionNotAwaitingMediaDownloadConstraint
}
}
}

View File

@@ -15,12 +15,14 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.DeletionNotAwaitingMediaDownloadConstraint
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import kotlin.time.Duration.Companion.seconds
/** /**
* Handles deleting user backup and unsubscribing them from backups. * Handles deleting user backup and unsubscribing them from backups.
@@ -39,7 +41,9 @@ class BackupDeleteJob private constructor(
backupDeleteJobData, backupDeleteJobData,
Parameters.Builder() Parameters.Builder()
.addConstraint(NetworkConstraint.KEY) .addConstraint(NetworkConstraint.KEY)
.addConstraint(DeletionNotAwaitingMediaDownloadConstraint.KEY)
.setMaxInstancesForFactory(1) .setMaxInstancesForFactory(1)
.setMaxAttempts(Parameters.UNLIMITED)
.build() .build()
) )
@@ -48,11 +52,16 @@ class BackupDeleteJob private constructor(
override fun getFactoryKey(): String = KEY override fun getFactoryKey(): String = KEY
override fun run(): Result { override fun run(): Result {
if (SignalStore.backup.deletionState == DeletionState.NONE || SignalStore.backup.deletionState == DeletionState.FAILED || SignalStore.backup.deletionState == DeletionState.COMPLETE) { if (SignalStore.backup.deletionState.isIdle()) {
Log.w(TAG, "Invalid state ${SignalStore.backup.deletionState}. Exiting.") Log.w(TAG, "Invalid state ${SignalStore.backup.deletionState}. Exiting.")
return Result.failure() return Result.failure()
} }
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
Log.i(TAG, "Awaiting media download. Scheduling retry.")
return Result.retry(5.seconds.inWholeMilliseconds)
}
val clearLocalStateResult = if (SignalStore.backup.deletionState == DeletionState.CLEAR_LOCAL_STATE) { val clearLocalStateResult = if (SignalStore.backup.deletionState == DeletionState.CLEAR_LOCAL_STATE) {
val results = listOf( val results = listOf(
deleteLocalState(), deleteLocalState(),
@@ -70,13 +79,14 @@ class BackupDeleteJob private constructor(
} }
if (isMediaRestoreRequired()) { if (isMediaRestoreRequired()) {
Log.i(TAG, "Moving to AWAITING_MEDIA_DOWNLOAD state") Log.i(TAG, "Moving to AWAITING_MEDIA_DOWNLOAD state and scheduling retry.")
SignalStore.backup.deletionState = DeletionState.AWAITING_MEDIA_DOWNLOAD SignalStore.backup.deletionState = DeletionState.AWAITING_MEDIA_DOWNLOAD
AppDependencies.jobManager AppDependencies.jobManager
.startChain(RestoreOptimizedMediaJob()) .startChain(BackupRestoreMediaJob())
.then(BackupDeleteJob(backupDeleteJobData)) .then(RestoreOptimizedMediaJob())
.enqueue()
return Result.failure() return Result.retry(5.seconds.inWholeMilliseconds)
} }
Log.i(TAG, "Moving to DELETE_BACKUPS state") Log.i(TAG, "Moving to DELETE_BACKUPS state")
@@ -126,7 +136,9 @@ class BackupDeleteJob private constructor(
private fun isMediaRestoreRequired(): Boolean { private fun isMediaRestoreRequired(): Boolean {
val requiresMediaRestore = SignalDatabase.attachments.getRemainingRestorableAttachmentSize() > 0L val requiresMediaRestore = SignalDatabase.attachments.getRemainingRestorableAttachmentSize() > 0L
if (requiresMediaRestore && SignalStore.backup.userManuallySkippedMediaRestore) { val hasOffloadedMedia = SignalDatabase.attachments.getOptimizedMediaAttachmentSize() > 0L
if ((requiresMediaRestore || hasOffloadedMedia) && !SignalStore.backup.userManuallySkippedMediaRestore) {
Log.i(TAG, "User has undownloaded media. Enqueuing download now.") Log.i(TAG, "User has undownloaded media. Enqueuing download now.")
return true return true
} else { } else {
@@ -224,6 +236,7 @@ class BackupDeleteJob private constructor(
SignalDatabase.recipients.markNeedsSync(Recipient.self().id) SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange() StorageSyncHelper.scheduleSyncForDataChange()
SignalDatabase.attachments.clearAllArchiveData() SignalDatabase.attachments.clearAllArchiveData()
SignalStore.backup.optimizeStorage = false
addStageToCompletions(BackupDeleteJobData.Stage.CLEAR_LOCAL_STATE) addStageToCompletions(BackupDeleteJobData.Stage.CLEAR_LOCAL_STATE)
return Result.success() return Result.success()
} }

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.jobs package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.RestoreState import org.thoughtcrime.securesms.backup.RestoreState
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.Job
@@ -44,6 +45,10 @@ class CheckRestoreMediaLeftJob private constructor(parameters: Parameters) : Job
Log.d(TAG, "Media restore complete: there are no remaining restorable attachments.") Log.d(TAG, "Media restore complete: there are no remaining restorable attachments.")
SignalStore.backup.totalRestorableAttachmentSize = 0 SignalStore.backup.totalRestorableAttachmentSize = 0
SignalStore.backup.restoreState = RestoreState.NONE SignalStore.backup.restoreState = RestoreState.NONE
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED
}
} else if (runAttempt == 0) { } else if (runAttempt == 0) {
Log.w(TAG, "Still have remaining data to restore, will retry before checking job queues, queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize") Log.w(TAG, "Still have remaining data to restore, will retry before checking job queues, queue: ${parameters.queue} estimated remaining: $remainingAttachmentSize")
return Result.retry(15.seconds.inWholeMilliseconds) return Result.retry(15.seconds.inWholeMilliseconds)

View File

@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.DeletionNotAwaitingMediaDownloadConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint;
@@ -412,6 +413,7 @@ public final class JobManagerFactories {
put(ChargingConstraint.KEY, new ChargingConstraint.Factory()); put(ChargingConstraint.KEY, new ChargingConstraint.Factory());
put(DataRestoreConstraint.KEY, new DataRestoreConstraint.Factory()); put(DataRestoreConstraint.KEY, new DataRestoreConstraint.Factory());
put(DecryptionsDrainedConstraint.KEY, new DecryptionsDrainedConstraint.Factory()); put(DecryptionsDrainedConstraint.KEY, new DecryptionsDrainedConstraint.Factory());
put(DeletionNotAwaitingMediaDownloadConstraint.KEY, new DeletionNotAwaitingMediaDownloadConstraint.Factory());
put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application)); put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application));
put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application)); put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application));
put(NetworkOrCellServiceConstraint.LEGACY_KEY, new NetworkOrCellServiceConstraint.Factory(application)); put(NetworkOrCellServiceConstraint.LEGACY_KEY, new NetworkOrCellServiceConstraint.Factory(application));
@@ -435,7 +437,8 @@ public final class JobManagerFactories {
RestoreAttachmentConstraintObserver.INSTANCE, RestoreAttachmentConstraintObserver.INSTANCE,
NoRemoteArchiveGarbageCollectionPendingConstraint.Observer.INSTANCE, NoRemoteArchiveGarbageCollectionPendingConstraint.Observer.INSTANCE,
RegisteredConstraint.Observer.INSTANCE, RegisteredConstraint.Observer.INSTANCE,
BackupMessagesConstraintObserver.INSTANCE); BackupMessagesConstraintObserver.INSTANCE,
DeletionNotAwaitingMediaDownloadConstraint.Observer.INSTANCE);
} }
public static List<JobMigration> getJobMigrations(@NonNull Application application) { public static List<JobMigration> getJobMigrations(@NonNull Application application) {

View File

@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.jobmanager.impl.BackupMessagesConstraintObserver import org.thoughtcrime.securesms.jobmanager.impl.BackupMessagesConstraintObserver
import org.thoughtcrime.securesms.jobmanager.impl.DeletionNotAwaitingMediaDownloadConstraint
import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollectionPendingConstraint import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollectionPendingConstraint
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
@@ -100,7 +101,17 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var lastBackupProtoSize: Long by longValue(KEY_BACKUP_LAST_PROTO_SIZE, 0L) var lastBackupProtoSize: Long by longValue(KEY_BACKUP_LAST_PROTO_SIZE, 0L)
private val deletionStateValue = enumValue(KEY_BACKUP_DELETION_STATE, DeletionState.NONE, DeletionState.serializer) private val deletionStateValue = enumValue(KEY_BACKUP_DELETION_STATE, DeletionState.NONE, DeletionState.serializer)
var deletionState by deletionStateValue private var internalDeletionState by deletionStateValue
var deletionState: DeletionState
get() {
return internalDeletionState
}
set(value) {
internalDeletionState = value
DeletionNotAwaitingMediaDownloadConstraint.Observer.notifyListeners()
}
val deletionStateFlow: Flow<DeletionState> = deletionStateValue.toFlow() val deletionStateFlow: Flow<DeletionState> = deletionStateValue.toFlow()
var restoreState: RestoreState by enumValue(KEY_RESTORE_STATE, RestoreState.NONE, RestoreState.serializer) var restoreState: RestoreState by enumValue(KEY_RESTORE_STATE, RestoreState.NONE, RestoreState.serializer)

View File

@@ -184,9 +184,11 @@ object Dialogs {
* let the user know that some action is completing. * let the user know that some action is completing.
*/ */
@Composable @Composable
fun IndeterminateProgressDialog() { fun IndeterminateProgressDialog(
onDismissRequest: () -> Unit = {}
) {
BaseAlertDialog( BaseAlertDialog(
onDismissRequest = {}, onDismissRequest = onDismissRequest,
confirmButton = {}, confirmButton = {},
dismissButton = {}, dismissButton = {},
text = { text = {