diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/DeletionState.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/DeletionState.kt index 308cd80bc0..da8d4c6e12 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/DeletionState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/DeletionState.kt @@ -35,6 +35,11 @@ enum class DeletionState(private val id: Int) { */ 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. * User should see the "deleting backups..." UX @@ -47,6 +52,12 @@ enum class DeletionState(private val id: Int) { */ COMPLETE(3); + fun isInProgress(): Boolean { + return this != FAILED && this != NONE && this != COMPLETE + } + + fun isIdle(): Boolean = !isInProgress() + companion object { val serializer: LongSerializer = Serializer() } @@ -56,11 +67,12 @@ enum class DeletionState(private val id: Int) { return data.id.toLong() } - override fun deserialize(data: Long): DeletionState { - return when (data.toInt()) { + override fun deserialize(input: Long): DeletionState { + return when (input.toInt()) { FAILED.id -> FAILED CLEAR_LOCAL_STATE.id -> CLEAR_LOCAL_STATE AWAITING_MEDIA_DOWNLOAD.id -> AWAITING_MEDIA_DOWNLOAD + MEDIA_DOWNLOAD_FINISHED.id -> MEDIA_DOWNLOAD_FINISHED DELETE_BACKUPS.id -> DELETE_BACKUPS COMPLETE.id -> COMPLETE else -> NONE diff --git a/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt index 55e1de2819..9e8232b826 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/billing/upgrade/UpgradeToPaidTierBottomSheet.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.rx3.asFlowable import org.signal.core.ui.compose.Dialogs import org.signal.core.util.concurrent.SignalDispatchers import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.backup.DeletionState import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowViewModel import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsStage @@ -80,7 +79,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment() viewLifecycleOwner.lifecycleScope.launch(SignalDispatchers.Main) { repeatOnLifecycle(Lifecycle.State.RESUMED) { viewModel.deletionState.collectLatest { - if (it == DeletionState.DELETE_BACKUPS) { + if (it.isInProgress()) { Toast.makeText( requireContext(), R.string.MessageBackupsFlowFragment__a_backup_deletion_is_in_progress, 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 17cb7ea2bf..2074c4cc8a 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 @@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth 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.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties import androidx.fragment.app.setFragmentResultListener import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController @@ -487,7 +485,7 @@ private fun RemoteBackupsSettingsContent( state = state.backupState, onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription, onRenewClick = contentCallbacks::onRenewLostSubscription, - isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS + isRenewEnabled = backupDeleteState.isIdle() ) } @@ -497,7 +495,7 @@ private fun RemoteBackupsSettingsContent( BackupCard( backupState = state.backupState, 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), onRenewClick = contentCallbacks::onRenewLostSubscription, onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription, - isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS + isRenewEnabled = backupDeleteState.isIdle() ) } @@ -535,6 +533,7 @@ private fun RemoteBackupsSettingsContent( canRestoreUsingCellular = state.canRestoreUsingCellular, canBackUpNow = !state.isOutOfStorageSpace, includeDebuglog = state.includeDebuglog, + backupMediaDetails = state.backupMediaDetails, contentCallbacks = contentCallbacks ) } else { @@ -591,7 +590,7 @@ private fun RemoteBackupsSettingsContent( } RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER -> { - CircularProgressDialog(onDismiss = contentCallbacks::onDialogDismissed) + Dialogs.IndeterminateProgressDialog(onDismissRequest = { contentCallbacks.onDialogDismissed() }) } RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> { @@ -764,13 +763,13 @@ private fun LazyListScope.appendBackupDeletionItems( } else { item { LinearProgressIndicator( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.horizontalGutters().fillMaxWidth() ) } } } - DeletionState.DELETE_BACKUPS -> { + DeletionState.MEDIA_DOWNLOAD_FINISHED, DeletionState.DELETE_BACKUPS -> { item { DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data)) } @@ -841,6 +840,7 @@ private fun LazyListScope.appendBackupDetailsItems( canRestoreUsingCellular: Boolean, canBackUpNow: Boolean, includeDebuglog: Boolean?, + backupMediaDetails: RemoteBackupsSettingsState.BackupMediaDetails?, contentCallbacks: ContentCallbacks ) { item { @@ -851,6 +851,16 @@ private fun LazyListScope.appendBackupDetailsItems( 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.FromBackupStatusData) { 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) @Composable private fun BackupFrequencyDialog( @@ -2122,16 +2103,6 @@ private fun SkipDownloadDialogPreview() { } } -@SignalPreview -@Composable -private fun CircularProgressDialogPreview() { - Previews.Preview { - CircularProgressDialog( - onDismiss = {} - ) - } -} - @SignalPreview @Composable private fun BackupFrequencyDialogPreview() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt index 5cb6dcafda..87da0c9b69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt @@ -5,6 +5,7 @@ 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.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.backups.BackupState @@ -28,9 +29,15 @@ data class RemoteBackupsSettingsState( val dialog: Dialog = Dialog.NONE, val snackbar: Snackbar = Snackbar.NONE, 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 { NONE, TURN_OFF_AND_DELETE_BACKUPS, 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 404e77dcd1..82a08297a4 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 @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.reactive.asFlow +import kotlinx.coroutines.withContext import org.signal.core.util.bytes import org.signal.core.util.logging.Log 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.protos.ArchiveUploadProgressState import org.thoughtcrime.securesms.service.MessageBackupListener +import org.thoughtcrime.securesms.util.Environment import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.NetworkResult @@ -81,7 +83,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { init { viewModelScope.launch(Dispatchers.IO) { - _state.update { it.copy(backupMediaSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize()) } + refreshBackupMediaSizeState() } viewModelScope.launch(Dispatchers.IO) { @@ -105,7 +107,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { .attachmentUpdates() .throttleLatest(5.seconds) .collectLatest { - _state.update { it.copy(backupMediaSize = SignalDatabase.attachments.getEstimatedArchiveMediaSize()) } + refreshBackupMediaSizeState() } } @@ -209,10 +211,14 @@ class RemoteBackupsSettingsViewModel : ViewModel() { } fun turnOffAndDeleteBackups() { - requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER) + viewModelScope.launch { + requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER) - viewModelScope.launch(Dispatchers.IO) { - BackupRepository.turnOffAndDisableBackups() + withContext(Dispatchers.IO) { + BackupRepository.turnOffAndDisableBackups() + } + + requestDialog(RemoteBackupsSettingsState.Dialog.NONE) } } @@ -229,6 +235,20 @@ class RemoteBackupsSettingsViewModel : ViewModel() { _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?) { try { Log.i(TAG, "Performing a state refresh.") diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/DeletionNotAwaitingMediaDownloadConstraint.kt b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/DeletionNotAwaitingMediaDownloadConstraint.kt new file mode 100644 index 0000000000..08e7d77424 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/impl/DeletionNotAwaitingMediaDownloadConstraint.kt @@ -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 = mutableSetOf() + + override fun register(notifier: ConstraintObserver.Notifier) { + listeners += notifier + } + + fun notifyListeners() { + for (listener in listeners) { + listener.onConstraintMet(KEY) + } + } + } + + class Factory : Constraint.Factory { + override fun create(): DeletionNotAwaitingMediaDownloadConstraint { + return DeletionNotAwaitingMediaDownloadConstraint + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupDeleteJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupDeleteJob.kt index 34b91f7ea3..078b650071 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupDeleteJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupDeleteJob.kt @@ -15,12 +15,14 @@ import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.DeletionNotAwaitingMediaDownloadConstraint import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.whispersystems.signalservice.api.NetworkResult +import kotlin.time.Duration.Companion.seconds /** * Handles deleting user backup and unsubscribing them from backups. @@ -39,7 +41,9 @@ class BackupDeleteJob private constructor( backupDeleteJobData, Parameters.Builder() .addConstraint(NetworkConstraint.KEY) + .addConstraint(DeletionNotAwaitingMediaDownloadConstraint.KEY) .setMaxInstancesForFactory(1) + .setMaxAttempts(Parameters.UNLIMITED) .build() ) @@ -48,11 +52,16 @@ class BackupDeleteJob private constructor( override fun getFactoryKey(): String = KEY 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.") 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 results = listOf( deleteLocalState(), @@ -70,13 +79,14 @@ class BackupDeleteJob private constructor( } 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 AppDependencies.jobManager - .startChain(RestoreOptimizedMediaJob()) - .then(BackupDeleteJob(backupDeleteJobData)) + .startChain(BackupRestoreMediaJob()) + .then(RestoreOptimizedMediaJob()) + .enqueue() - return Result.failure() + return Result.retry(5.seconds.inWholeMilliseconds) } Log.i(TAG, "Moving to DELETE_BACKUPS state") @@ -126,7 +136,9 @@ class BackupDeleteJob private constructor( private fun isMediaRestoreRequired(): Boolean { 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.") return true } else { @@ -224,6 +236,7 @@ class BackupDeleteJob private constructor( SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() SignalDatabase.attachments.clearAllArchiveData() + SignalStore.backup.optimizeStorage = false addStageToCompletions(BackupDeleteJobData.Stage.CLEAR_LOCAL_STATE) return Result.success() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckRestoreMediaLeftJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckRestoreMediaLeftJob.kt index bab0bdfbbe..0dc6596412 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckRestoreMediaLeftJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CheckRestoreMediaLeftJob.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.jobs import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.backup.DeletionState import org.thoughtcrime.securesms.backup.RestoreState import org.thoughtcrime.securesms.database.SignalDatabase 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.") SignalStore.backup.totalRestorableAttachmentSize = 0 SignalStore.backup.restoreState = RestoreState.NONE + + if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) { + SignalStore.backup.deletionState = DeletionState.MEDIA_DOWNLOAD_FINISHED + } } 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") return Result.retry(15.seconds.inWholeMilliseconds) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 9a84fe6144..8e88aa57c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint; import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.DecryptionsDrainedConstraint; 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.NetworkConstraintObserver; import org.thoughtcrime.securesms.jobmanager.impl.NetworkOrCellServiceConstraint; @@ -412,6 +413,7 @@ public final class JobManagerFactories { put(ChargingConstraint.KEY, new ChargingConstraint.Factory()); put(DataRestoreConstraint.KEY, new DataRestoreConstraint.Factory()); put(DecryptionsDrainedConstraint.KEY, new DecryptionsDrainedConstraint.Factory()); + put(DeletionNotAwaitingMediaDownloadConstraint.KEY, new DeletionNotAwaitingMediaDownloadConstraint.Factory()); put(NetworkConstraint.KEY, new NetworkConstraint.Factory(application)); put(NetworkOrCellServiceConstraint.KEY, new NetworkOrCellServiceConstraint.Factory(application)); put(NetworkOrCellServiceConstraint.LEGACY_KEY, new NetworkOrCellServiceConstraint.Factory(application)); @@ -435,7 +437,8 @@ public final class JobManagerFactories { RestoreAttachmentConstraintObserver.INSTANCE, NoRemoteArchiveGarbageCollectionPendingConstraint.Observer.INSTANCE, RegisteredConstraint.Observer.INSTANCE, - BackupMessagesConstraintObserver.INSTANCE); + BackupMessagesConstraintObserver.INSTANCE, + DeletionNotAwaitingMediaDownloadConstraint.Observer.INSTANCE); } public static List getJobMigrations(@NonNull Application application) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index a00d563c9c..f946b8ef32 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.MessageBackupTier 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.RestoreAttachmentConstraintObserver 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) 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 = deletionStateValue.toFlow() var restoreState: RestoreState by enumValue(KEY_RESTORE_STATE, RestoreState.NONE, RestoreState.serializer) diff --git a/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt b/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt index b2333327d8..b420232ce2 100644 --- a/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt +++ b/core-ui/src/main/java/org/signal/core/ui/compose/Dialogs.kt @@ -184,9 +184,11 @@ object Dialogs { * let the user know that some action is completing. */ @Composable - fun IndeterminateProgressDialog() { + fun IndeterminateProgressDialog( + onDismissRequest: () -> Unit = {} + ) { BaseAlertDialog( - onDismissRequest = {}, + onDismissRequest = onDismissRequest, confirmButton = {}, dismissButton = {}, text = {