Polish deletion UX.

This commit is contained in:
Alex Hart
2025-05-30 15:33:44 -03:00
committed by Cody Henthorne
parent ccce37d023
commit df170dac32
12 changed files with 315 additions and 109 deletions

View File

@@ -10,10 +10,42 @@ import org.signal.core.util.LongSerializer
/**
* Denotes the deletion state for backups.
*/
enum class DeletionState(val id: Int) {
enum class DeletionState(private val id: Int) {
/**
* Something bad happened, and the deletion could not be performed.
* User should see "backup failed" UX
*/
FAILED(-1),
/**
* No pending, running, failed, or completed deletion.
* User should not see UX specific to backup deletions.
*/
NONE(0),
RUNNING(1);
/**
* Clear local backup state and delete subscription.
* User should see a progress spinner.
*/
CLEAR_LOCAL_STATE(4),
/**
* Waiting to download media before deletion.
* User should see the "restoring media" progress UX
*/
AWAITING_MEDIA_DOWNLOAD(1),
/**
* Deleting the backups themselves.
* User should see the "deleting backups..." UX
*/
DELETE_BACKUPS(2),
/**
* Completed deletion.
* User should see the "backups deleted" UX
*/
COMPLETE(3);
companion object {
val serializer: LongSerializer<DeletionState> = Serializer()
@@ -27,7 +59,10 @@ enum class DeletionState(val id: Int) {
override fun deserialize(data: Long): DeletionState {
return when (data.toInt()) {
FAILED.id -> FAILED
RUNNING.id -> RUNNING
CLEAR_LOCAL_STATE.id -> CLEAR_LOCAL_STATE
AWAITING_MEDIA_DOWNLOAD.id -> AWAITING_MEDIA_DOWNLOAD
DELETE_BACKUPS.id -> DELETE_BACKUPS
COMPLETE.id -> COMPLETE
else -> NONE
}
}

View File

@@ -423,14 +423,16 @@ object BackupRepository {
*/
suspend fun turnOffAndDisableBackups() {
ArchiveUploadProgress.cancelAndBlock()
SignalStore.backup.userManuallySkippedMediaRestore = false
SignalStore.backup.deletionState = DeletionState.CLEAR_LOCAL_STATE
AppDependencies.jobManager.add(BackupDeleteJob())
}
SignalStore.backup.deletionState = DeletionState.RUNNING
SignalStore.backup.optimizeStorage = false
AppDependencies.jobManager
.startChain(RestoreOptimizedMediaJob())
.then(BackupDeleteJob())
.enqueue()
/**
* To be called if the user skips media restore during the deletion process.
*/
fun continueTurningOffAndDisablingBackups() {
AppDependencies.jobManager.add(BackupDeleteJob())
}
private fun createSignalDatabaseSnapshot(baseName: String): SignalDatabase {

View File

@@ -9,6 +9,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@@ -52,7 +53,7 @@ private val YELLOW_DOT = Color(0xFFFFCC00)
fun BackupStatusRow(
backupStatusData: BackupStatusData,
onSkipClick: () -> Unit = {},
onCancelClick: () -> Unit = {},
onCancelClick: (() -> Unit)? = null,
onLearnMoreClick: () -> Unit = {}
) {
Column {
@@ -61,7 +62,7 @@ fun BackupStatusRow(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)).defaultMinSize(minHeight = 48.dp)
) {
LinearProgressIndicator(
color = progressColor(backupStatusData),
@@ -69,13 +70,15 @@ fun BackupStatusRow(
modifier = Modifier.weight(1f)
)
IconButton(
onClick = onCancelClick
) {
Icon(
painter = painterResource(R.drawable.symbol_x_24),
contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
)
if (onCancelClick != null) {
IconButton(
onClick = onCancelClick
) {
Icon(
painter = painterResource(R.drawable.symbol_x_24),
contentDescription = stringResource(R.string.BackupStatusRow__cancel_download)
)
}
}
}
}

View File

@@ -78,7 +78,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
viewLifecycleOwner.lifecycleScope.launch(SignalDispatchers.Main) {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.deletionState.collectLatest {
if (it == DeletionState.RUNNING) {
if (it == DeletionState.DELETE_BACKUPS) {
Toast.makeText(
requireContext(),
R.string.MessageBackupsFlowFragment__a_backup_deletion_is_in_progress,

View File

@@ -80,7 +80,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
viewLifecycleOwner.lifecycleScope.launch(SignalDispatchers.Main) {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.deletionState.collectLatest {
if (it == DeletionState.RUNNING) {
if (it == DeletionState.DELETE_BACKUPS) {
Toast.makeText(
requireContext(),
R.string.MessageBackupsFlowFragment__a_backup_deletion_is_in_progress,

View File

@@ -50,7 +50,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
@@ -275,6 +274,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
override fun onRedemptionErrorDetailsClick() {
BackupAlertBottomSheet.create(BackupAlert.CouldNotRedeemBackup).show(parentFragmentManager, null)
}
override fun onDisplayProgressDialog() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
}
}
private fun displayBackupKey() {
@@ -361,6 +364,7 @@ private interface ContentCallbacks {
fun onRestoreUsingCellularConfirm() = Unit
fun onRestoreUsingCellularClick() = Unit
fun onRedemptionErrorDetailsClick() = Unit
fun onDisplayProgressDialog() = Unit
object Emtpy : ContentCallbacks
}
@@ -390,6 +394,12 @@ private fun RemoteBackupsSettingsContent(
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
LaunchedEffect(backupDeleteState) {
if (backupDeleteState != DeletionState.NONE && backupDeleteState != DeletionState.CLEAR_LOCAL_STATE) {
contentCallbacks.onDialogDismissed()
}
}
Scaffold(
topBar = {
Scaffolds.DefaultTopAppBar(
@@ -449,7 +459,7 @@ private fun RemoteBackupsSettingsContent(
state = backupState,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
onRenewClick = contentCallbacks::onRenewLostSubscription,
isRenewEnabled = backupDeleteState != DeletionState.RUNNING
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
)
}
@@ -459,7 +469,7 @@ private fun RemoteBackupsSettingsContent(
BackupCard(
backupState = backupState,
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick,
buttonsEnabled = backupDeleteState != DeletionState.RUNNING
buttonsEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
)
}
@@ -468,14 +478,19 @@ private fun RemoteBackupsSettingsContent(
title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
onRenewClick = contentCallbacks::onRenewLostSubscription,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
isRenewEnabled = backupDeleteState != DeletionState.RUNNING
isRenewEnabled = backupDeleteState != DeletionState.DELETE_BACKUPS
)
}
}
}
if (backupDeleteState != DeletionState.NONE) {
appendBackupDeletionState(backupDeleteState, contentCallbacks)
if (backupDeleteState != DeletionState.NONE && backupDeleteState != DeletionState.CLEAR_LOCAL_STATE) {
appendBackupDeletionItems(
backupDeleteState = backupDeleteState,
backupRestoreState = backupRestoreState,
canRestoreUsingCellular = canRestoreUsingCellular,
contentCallbacks = contentCallbacks
)
} else if (backupsEnabled) {
appendBackupDetailsItems(
backupState = backupState,
@@ -619,22 +634,42 @@ private fun ReenableBackupsButton(contentCallbacks: ContentCallbacks) {
}
}
private fun LazyListScope.appendBackupDeletionState(
private fun LazyListScope.appendRestoreFromBackupStatusData(
backupRestoreState: BackupRestoreState.FromBackupStatusData,
canRestoreUsingCellular: Boolean,
contentCallbacks: ContentCallbacks
) {
item {
BackupStatusRow(
backupStatusData = backupRestoreState.backupStatusData,
onCancelClick = contentCallbacks::onCancelMediaRestore,
onSkipClick = contentCallbacks::onSkipMediaRestore,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
)
}
if (!canRestoreUsingCellular) {
item {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
onClick = contentCallbacks::onRestoreUsingCellularConfirm
)
}
}
}
private fun LazyListScope.appendBackupDeletionItems(
backupDeleteState: DeletionState,
backupRestoreState: BackupRestoreState,
canRestoreUsingCellular: Boolean,
contentCallbacks: ContentCallbacks
) {
when (backupDeleteState) {
DeletionState.NONE -> return
DeletionState.FAILED -> {
item {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_but_there_was_an_error),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
)
DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_but_there_was_an_error))
}
item {
@@ -665,18 +700,33 @@ private fun LazyListScope.appendBackupDeletionState(
}
}
DeletionState.RUNNING -> {
DeletionState.AWAITING_MEDIA_DOWNLOAD -> {
item {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
DescriptionText(
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_your_data_will_be)
)
}
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
appendRestoreFromBackupStatusData(
backupRestoreState = backupRestoreState,
canRestoreUsingCellular = canRestoreUsingCellular,
contentCallbacks = contentCallbacks
)
} else {
item {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
}
}
DeletionState.DELETE_BACKUPS -> {
item {
DescriptionText(text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data))
}
item {
Column(
verticalArrangement = spacedBy(12.dp),
@@ -696,9 +746,40 @@ private fun LazyListScope.appendBackupDeletionState(
}
}
}
DeletionState.COMPLETE -> {
item {
DescriptionText(
text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data_has_been_deleted),
modifier = Modifier.padding(bottom = 12.dp)
)
}
item {
ReenableBackupsButton(contentCallbacks)
}
}
DeletionState.CLEAR_LOCAL_STATE -> Unit
}
}
@Composable
private fun DescriptionText(
text: String,
modifier: Modifier = Modifier
) {
Text(
text = text,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 16.dp)
.then(modifier)
)
}
private fun LazyListScope.appendBackupDetailsItems(
backupState: RemoteBackupsSettingsState.BackupState,
backupRestoreState: BackupRestoreState,
@@ -720,24 +801,11 @@ private fun LazyListScope.appendBackupDetailsItems(
if (backupRestoreState !is BackupRestoreState.None) {
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
item {
BackupStatusRow(
backupStatusData = backupRestoreState.backupStatusData,
onCancelClick = contentCallbacks::onCancelMediaRestore,
onSkipClick = contentCallbacks::onSkipMediaRestore,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
)
}
if (!canRestoreUsingCellular) {
item {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
onClick = contentCallbacks::onRestoreUsingCellularConfirm
)
}
}
appendRestoreFromBackupStatusData(
backupRestoreState = backupRestoreState,
canRestoreUsingCellular = canRestoreUsingCellular,
contentCallbacks = contentCallbacks
)
} else if (backupRestoreState is BackupRestoreState.Ready) {
item {
BackupReadyToDownloadRow(
@@ -1363,7 +1431,7 @@ private fun TurnOffAndDeleteBackupsDialog(
) {
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete_backups),
body = stringResource(id = R.string.RemoteBackupsSettingsFragment__you_will_not_be_charged_again),
body = stringResource(id = R.string.RemoteBackupsSettingsFragment__your_backup_will_be_deleted_and_no_new_backups_will_be_created),
confirm = stringResource(id = R.string.RemoteBackupsSettingsFragment__turn_off_and_delete),
dismiss = stringResource(id = android.R.string.cancel),
confirmColor = MaterialTheme.colorScheme.error,
@@ -1879,28 +1947,31 @@ private fun BackupFrequencyDialogPreview() {
@SignalPreview
@Composable
private fun BackupDeletionCardPreview() {
var backupDeletionState by remember { mutableStateOf(DeletionState.NONE) }
Previews.Preview {
LazyColumn {
item {
Buttons.MediumTonal(
onClick = {
backupDeletionState = when (backupDeletionState) {
DeletionState.FAILED -> DeletionState.NONE
DeletionState.NONE -> DeletionState.RUNNING
DeletionState.RUNNING -> DeletionState.FAILED
}
}
) {
Text("Next Deletion State")
for (state in DeletionState.entries.filter { it.hasUx() }) {
appendBackupDeletionItems(
backupDeleteState = state,
backupRestoreState = BackupRestoreState.FromBackupStatusData(
backupStatusData = BackupStatusData.RestoringMedia(
bytesDownloaded = 80.mebiBytes,
bytesTotal = 3.gibiBytes
)
),
contentCallbacks = ContentCallbacks.Emtpy,
canRestoreUsingCellular = true
)
item {
Dividers.Default()
}
}
appendBackupDeletionState(backupDeletionState, contentCallbacks = ContentCallbacks.Emtpy)
}
}
}
private fun DeletionState.hasUx() = this !in setOf(DeletionState.NONE, DeletionState.CLEAR_LOCAL_STATE)
private fun ArchiveUploadProgressState.frameExportProgress(): Float {
return if (this.frameTotalCount == 0L) {
0f

View File

@@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
@@ -47,6 +46,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.service.MessageBackupListener
import java.util.Currency
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
@@ -78,11 +78,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
init {
viewModelScope.launch(Dispatchers.IO) {
SignalStore.backup.deletionStateFlow
.filter { it == DeletionState.NONE }
.collect {
refresh()
}
SignalStore.backup.deletionStateFlow.collectLatest {
refresh()
}
}
viewModelScope.launch(Dispatchers.IO) {
@@ -164,6 +162,10 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
fun skipMediaRestore() {
BackupRepository.skipMediaRestore()
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
BackupRepository.continueTurningOffAndDisablingBackups()
}
}
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
@@ -187,6 +189,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
fun turnOffAndDeleteBackups() {
requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER)
viewModelScope.launch(Dispatchers.IO) {
BackupRepository.turnOffAndDisableBackups()
}
@@ -202,6 +206,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
try {
Log.i(TAG, "Performing a state refresh.")
performStateRefresh(lastPurchase)
} catch (e: Exception) {
Log.w(TAG, "State refresh failed", e)
@@ -241,7 +246,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
Log.d(TAG, "[subscriptionStateMismatchDetected] A mismatch was detected.")
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
is BillingPurchaseResult.Success -> purchaseResult.isAcknowledged && purchaseResult.isWithinTheLastMonth()
is BillingPurchaseResult.Success -> purchaseResult.isAcknowledged && purchaseResult.isWithinTheLastMonth() && purchaseResult.isAutoRenewing
else -> false
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
@@ -339,6 +344,15 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
backupState = RemoteBackupsSettingsState.BackupState.NotFound
)
}
} else if (lastPurchase != null && lastPurchase.endOfPeriod > System.currentTimeMillis().milliseconds) {
_state.update {
it.copy(
backupState = RemoteBackupsSettingsState.BackupState.Canceled(
messageBackupsType = type,
renewalTime = lastPurchase.endOfPeriod
)
)
}
} else {
_state.update {
it.copy(

View File

@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
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.NetworkConstraint
import org.thoughtcrime.securesms.jobs.protos.BackupDeleteJobData
@@ -32,8 +33,8 @@ class BackupDeleteJob private constructor(
private val TAG = Log.tag(BackupDeleteJob::class)
}
constructor() : this(
BackupDeleteJobData(),
constructor(backupDeleteJobData: BackupDeleteJobData = BackupDeleteJobData()) : this(
backupDeleteJobData,
Parameters.Builder()
.addConstraint(NetworkConstraint.KEY)
.setMaxInstancesForFactory(1)
@@ -44,25 +45,70 @@ class BackupDeleteJob private constructor(
override fun getFactoryKey(): String = KEY
override fun onAdded() {
SignalStore.backup.deletionState = DeletionState.RUNNING
}
override fun run(): Result {
if (SignalStore.backup.deletionState == DeletionState.NONE || SignalStore.backup.deletionState == DeletionState.FAILED || SignalStore.backup.deletionState == DeletionState.COMPLETE) {
Log.w(TAG, "Invalid state ${SignalStore.backup.deletionState}. Exiting.")
return Result.failure()
}
val clearLocalStateResult = if (SignalStore.backup.deletionState == DeletionState.CLEAR_LOCAL_STATE) {
val results = listOf(
deleteLocalState(),
cancelActiveSubscription()
)
checkResults(results)
} else {
Result.success()
}
if (!clearLocalStateResult.isSuccess) {
Log.w(TAG, "Failed to clear local state and subscriber.")
return clearLocalStateResult
}
if (isMediaRestoreRequired()) {
Log.i(TAG, "Moving to AWAITING_MEDIA_DOWNLOAD state")
SignalStore.backup.deletionState = DeletionState.AWAITING_MEDIA_DOWNLOAD
AppDependencies.jobManager
.startChain(RestoreOptimizedMediaJob())
.then(BackupDeleteJob(backupDeleteJobData))
return Result.failure()
}
Log.i(TAG, "Moving to DELETE_BACKUPS state")
SignalStore.backup.deletionState = DeletionState.DELETE_BACKUPS
val results = listOf(
cancelActiveSubscription(),
deleteMessageBackup(),
deleteMediaBackup(),
deleteLocalState()
deleteMediaBackup()
)
val result = checkResults(results)
if (result.isSuccess) {
Log.i(TAG, "Backup deletion was successful.")
SignalStore.backup.deletionState = DeletionState.COMPLETE
}
return result
}
override fun onFailure() {
if (SignalStore.backup.deletionState == DeletionState.AWAITING_MEDIA_DOWNLOAD) {
Log.w(TAG, "BackupDeleteFailure occurred while awaiting media download, ignoring.")
} else {
SignalStore.backup.deletionState = DeletionState.FAILED
}
}
private fun checkResults(results: List<Result>): Result {
val isAllSuccess = results.all { it.isSuccess }
val hasRetries = results.any { it.isRetry }
return when {
isAllSuccess -> {
Log.d(TAG, "All stages completed successfully.")
SignalStore.backup.deletionState = DeletionState.NONE
Log.d(TAG, "${results.size} stages completed successfully.")
Result.success()
}
hasRetries -> {
@@ -76,8 +122,15 @@ class BackupDeleteJob private constructor(
}
}
override fun onFailure() {
SignalStore.backup.deletionState = DeletionState.FAILED
private fun isMediaRestoreRequired(): Boolean {
val requiresMediaRestore = SignalDatabase.attachments.getRemainingRestorableAttachmentSize() > 0L
if (requiresMediaRestore && SignalStore.backup.userManuallySkippedMediaRestore) {
Log.i(TAG, "User has undownloaded media. Enqueuing download now.")
return true
} else {
Log.i(TAG, "User does not have undownloaded media or has opted to skip restoration.")
return false
}
}
private fun cancelActiveSubscription(): Result {
@@ -129,16 +182,7 @@ class BackupDeleteJob private constructor(
return Result.success()
}
Log.d(TAG, "Loading backup tier from service.")
val backupTierResult: NetworkResult<MessageBackupTier> = BackupRepository.getBackupTier()
if (backupTierResult.getCause() != null) {
return handleNetworkError(backupTierResult)
}
val backupTier: MessageBackupTier = backupTierResult.successOrThrow()
Log.d(TAG, "Network request returned $backupTier")
if (backupTier == MessageBackupTier.PAID) {
if (backupDeleteJobData.tier == BackupDeleteJobData.Tier.PAID) {
val deleteMediaBackupResult: NetworkResult<Unit> = BackupRepository.deleteMediaBackup()
if (deleteMediaBackupResult.getCause() != null) {
Log.w(TAG, "Failed to delete media backup", deleteMediaBackupResult.getCause())
@@ -158,6 +202,21 @@ class BackupDeleteJob private constructor(
return Result.success()
}
Log.d(TAG, "Loading backup tier from service.")
val backupTierResult: NetworkResult<MessageBackupTier> = BackupRepository.getBackupTier()
if (backupTierResult.getCause() != null) {
return handleNetworkError(backupTierResult)
}
val backupTier: MessageBackupTier = backupTierResult.successOrThrow()
Log.d(TAG, "Network request returned $backupTier")
backupDeleteJobData = backupDeleteJobData.newBuilder().tier(
when (backupTier) {
MessageBackupTier.FREE -> BackupDeleteJobData.Tier.FREE
MessageBackupTier.PAID -> BackupDeleteJobData.Tier.PAID
}
).build()
Log.d(TAG, "Clearing local backup state.")
SignalStore.backup.disableBackups()
SignalDatabase.attachments.clearAllArchiveData()

View File

@@ -11,6 +11,7 @@ import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.DeletionState
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
@@ -84,10 +85,16 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
return Result.success()
}
if (SignalStore.backup.deletionState != DeletionState.NONE) {
Log.i(TAG, "User is in the process of or has delete their backup. Clearing mismatch value and exiting.")
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
}
val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases()
Log.i(TAG, "Retrieved purchase result from Billing api: $purchase")
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth() && purchase.isAutoRenewing
val product: BillingProduct? = AppDependencies.billingApi.queryProduct()
if (product == null) {

View File

@@ -206,6 +206,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
.putLong(KEY_LATEST_BACKUP_TIER, serializedValue)
.putBoolean(KEY_BACKUP_TIER_RESTORED, true)
.apply()
deletionState = DeletionState.NONE
} else {
putLong(KEY_BACKUP_TIER, serializedValue)
}

View File

@@ -229,5 +229,12 @@ message BackupDeleteJobData {
CLEAR_LOCAL_STATE = 4;
}
enum Tier {
UNKNOWN_TIER = 0;
FREE = 1;
PAID = 2;
}
repeated Stage completed = 1;
Tier tier = 2;
}

View File

@@ -8130,7 +8130,7 @@
<!-- Dialog title when confirming whether to turn off and deleting backups -->
<string name="RemoteBackupsSettingsFragment__turn_off_and_delete_backups">Turn off and delete backups?</string>
<!-- Dialog message when confirming whether to turn off and delete backups -->
<string name="RemoteBackupsSettingsFragment__you_will_not_be_charged_again">You will not be charged again. Your backup will be deleted and no new backups will be created.</string>
<string name="RemoteBackupsSettingsFragment__your_backup_will_be_deleted_and_no_new_backups_will_be_created">Your backup will be deleted and no new backups will be created. Any media stored in your backup will be downloaded to your phone now.</string>
<!-- Confirmation action on dialog to turn off and delete backups -->
<string name="RemoteBackupsSettingsFragment__turn_off_and_delete">Turn off and delete</string>
<!-- Text on dialog while user backup is being deleted -->
@@ -8213,8 +8213,14 @@
<string name="RemoteBackupsSettingsFragment__failed_to_delete_backup">Failed to delete backup</string>
<!-- Text displayed as the body in a card when backup fails to delete -->
<string name="RemoteBackupsSettingsFragment__an_error_occurred_please_contact_support">An error occurred. Please contact support.</string>
<!-- Text displayed while we are processing a backup delete but are or need to download optimized media -->
<string name="RemoteBackupsSettingsFragment__backups_have_been_turned_off_your_data_will_be">Backups have been turned off, your data will download to your device and then be deleted from Signal\'s secure storage service.</string>
<!-- Text displayed while we are processing a backup delete -->
<string name="RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data">Backups have been turned off and your data will be deleted from Signal\'s secure storage service.</string>
<!-- Text displayed after we have deleted a user's backup -->
<string name="RemoteBackupsSettingsFragment__backups_have_been_turned_off_and_your_data_has_been_deleted">Backups have been turned off and your data has been deleted from Signals secure storage service.</string>
<!-- Text displayed while downloading backup during deletion. First placeholder is amount downloaded, second is amount remaining, last is percentage. -->
<string name="RemoteBackupsSettingsFragment__downloading_s_of_s_d">Downloading: %1$s of %2$s (%3$d%%)</string>
<!-- SubscriptionNotFoundBottomSheet -->
<!-- Displayed as a bottom sheet title -->