mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Polish deletion UX.
This commit is contained in:
committed by
Cody Henthorne
parent
ccce37d023
commit
df170dac32
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 Signal’s 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 -->
|
||||
|
||||
Reference in New Issue
Block a user