mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 11:08:31 +00:00
Fix backups deletion pipeline.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,11 +211,15 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun turnOffAndDeleteBackups() {
|
fun turnOffAndDeleteBackups() {
|
||||||
|
viewModelScope.launch {
|
||||||
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
|
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
BackupRepository.turnOffAndDisableBackups()
|
BackupRepository.turnOffAndDisableBackups()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBackupNowClick() {
|
fun onBackupNowClick() {
|
||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user