diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 0d64251a1d..710890c5c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -119,16 +119,20 @@ object BackupRepository { } } + /** + * If the user is on a paid tier, this method will unsubscribe them from that tier. + * It will then disable backups. + * + * Returns true if we were successful, false otherwise. + */ @WorkerThread - fun turnOffAndDeleteBackup(): Boolean { + fun turnOffAndDisableBackups(): Boolean { return try { Log.d(TAG, "Attempting to disable backups.") - getBackupTier().runIfSuccessful { tier -> - if (tier == MessageBackupTier.PAID) { - Log.d(TAG, "User is currently on a paid tier. Canceling.") - RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) - Log.d(TAG, "Successfully canceled paid tier.") - } + if (SignalStore.backup.backupTier == MessageBackupTier.PAID) { + Log.d(TAG, "User is currently on a paid tier. Canceling.") + RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) + Log.d(TAG, "Successfully canceled paid tier.") } Log.d(TAG, "Disabling backups.") @@ -636,14 +640,11 @@ object BackupRepository { } /** - * If backups are initialized, this method will query the server for the current backup level. - * If backups are not initialized, this method will return either the stored tier or a 404 result. + * If backups are enabled, sync with the network. Otherwise, return a 404. */ fun getBackupTier(): NetworkResult { - return if (SignalStore.backup.backupsInitialized) { + return if (SignalStore.backup.areBackupsEnabled) { getBackupTier(Recipient.self().requireAci()) - } else if (SignalStore.backup.backupTier != null) { - NetworkResult.Success(SignalStore.backup.backupTier!!) } else { NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(404)) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index 78580fde7a..8ce04bff5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -118,11 +118,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega currentBackupTier = state.currentMessageBackupTier, selectedBackupTier = state.selectedMessageBackupTier, availableBackupTypes = state.availableBackupTypes.filter { it.tier == MessageBackupTier.FREE || state.hasBackupSubscriberAvailable }, - onMessageBackupsTierSelected = { tier -> - val type = state.availableBackupTypes.first { it.tier == tier } - - viewModel.onMessageBackupTierUpdated(tier) - }, + onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated, onNavigationClick = viewModel::goToPreviousStage, onReadMoreClicked = {}, onNextClicked = viewModel::goToNextStage diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 960e032f61..ac70a77147 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -97,6 +97,13 @@ class MessageBackupsFlowViewModel( try { Log.d(TAG, "Attempting to handle successful purchase.") + + internalStateFlow.update { + it.copy( + stage = MessageBackupsStage.PROCESS_PAYMENT + ) + } + handleSuccess(result, id) internalStateFlow.update { diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt index aad635e31c..e2fda7ecd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt @@ -37,7 +37,7 @@ import org.thoughtcrime.securesms.util.safeUnregisterReceiver import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalCoroutinesApi::class) -class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener) : Banner() { +class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner() { private var totalRestoredSize: Long = 0 @@ -127,4 +127,9 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList fun onSkip() fun onDismissComplete() } + + private object EmptyListener : RestoreProgressBannerListener { + override fun onSkip() = Unit + override fun onDismissComplete() = Unit + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupRestoreState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupRestoreState.kt new file mode 100644 index 0000000000..531b992e84 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/BackupRestoreState.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.components.settings.app.backups.remote + +import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData + +/** + * State container for BackupStatusData, including the enabled state. + */ +data class BackupRestoreState( + val enabled: Boolean, + val backupStatusData: BackupStatusData +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index 967d7474ed..e9a9085c8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -11,6 +11,7 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -18,6 +19,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -31,7 +33,6 @@ import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState @@ -46,7 +47,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource @@ -76,6 +76,8 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.backup.ArchiveUploadProgress import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData +import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusRow import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher @@ -92,7 +94,6 @@ import org.thoughtcrime.securesms.util.viewModel import java.math.BigDecimal import java.util.Currency import java.util.Locale -import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds /** @@ -118,11 +119,11 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { override fun FragmentContent() { val state by viewModel.state.collectAsState() val backupProgress by ArchiveUploadProgress.progress.collectAsState(initial = null) + val restoreState by viewModel.restoreState.collectAsState() val callbacks = remember { Callbacks() } RemoteBackupsSettingsContent( - backupsInitialized = state.backupsInitialized, - messageBackupsType = state.messageBackupsType, + backupsEnabled = state.backupsEnabled, lastBackupTimestamp = state.lastBackupTimestamp, canBackUpUsingCellular = state.canBackUpUsingCellular, backupsFrequency = state.backupsFrequency, @@ -131,8 +132,8 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { contentCallbacks = callbacks, backupProgress = backupProgress, backupSize = state.backupSize, - renewalTime = state.renewalTime, - backupState = state.backupState + backupState = state.backupState, + backupRestoreState = restoreState ) } @@ -190,6 +191,14 @@ class RemoteBackupsSettingsFragment : ComposeFragment() { displayBackupKey() } } + + override fun onCancelMediaRestore() { + // TODO - [backups] Cancel media restoration + } + + override fun onSkipMediaRestore() { + // TODO - [backups] Skip media restoration + } } private fun displayBackupKey() { @@ -264,14 +273,15 @@ private interface ContentCallbacks { fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) = Unit fun onTurnOffAndDeleteBackupsConfirm() = Unit fun onViewBackupKeyClick() = Unit + fun onSkipMediaRestore() = Unit + fun onCancelMediaRestore() = Unit } @Composable private fun RemoteBackupsSettingsContent( - backupsInitialized: Boolean, - messageBackupsType: MessageBackupsType?, + backupsEnabled: Boolean, backupState: RemoteBackupsSettingsState.BackupState, - renewalTime: Duration, + backupRestoreState: BackupRestoreState, lastBackupTimestamp: Long, canBackUpUsingCellular: Boolean, backupsFrequency: BackupFrequency, @@ -297,26 +307,32 @@ private fun RemoteBackupsSettingsContent( modifier = Modifier .padding(it) ) { - if (backupState == RemoteBackupsSettingsState.BackupState.LOADING) { - item { - LoadingCard() - } - } else if (backupState == RemoteBackupsSettingsState.BackupState.ERROR) { - item { - ErrorCard() - } - } else if (messageBackupsType != null) { - item { - BackupCard( - messageBackupsType = messageBackupsType, - renewalTime = renewalTime, - backupState = backupState, - onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick - ) + item { + AnimatedContent(backupState, label = "backup-state-block") { state -> + when (state) { + is RemoteBackupsSettingsState.BackupState.Loading -> { + LoadingCard() + } + is RemoteBackupsSettingsState.BackupState.Error -> { + ErrorCard() + } + is RemoteBackupsSettingsState.BackupState.Pending -> { + PendingCard(state.price) + } + + RemoteBackupsSettingsState.BackupState.None -> Unit + + is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> { + BackupCard( + backupState = state, + onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick + ) + } + } } } - if (backupsInitialized) { + if (backupsEnabled) { appendBackupDetailsItems( backupProgress = backupProgress, lastBackupTimestamp = lastBackupTimestamp, @@ -326,13 +342,23 @@ private fun RemoteBackupsSettingsContent( contentCallbacks = contentCallbacks ) } else { - // TODO [backups] -- Download progress bar / state if required. + if (backupRestoreState.enabled) { + item { + BackupStatusRow( + backupStatusData = backupRestoreState.backupStatusData, + onCancelClick = contentCallbacks::onCancelMediaRestore, + onSkipClick = contentCallbacks::onSkipMediaRestore + ) + } + } item { Text( text = stringResource(R.string.RemoteBackupsSettingsFragment__backups_have_been_turned_off), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(top = 24.dp, bottom = 20.dp) + modifier = Modifier + .padding(top = 24.dp, bottom = 20.dp) + .horizontalGutters() ) } @@ -371,11 +397,12 @@ private fun RemoteBackupsSettingsContent( ) } - RemoteBackupsSettingsState.Dialog.DELETING_BACKUP, RemoteBackupsSettingsState.Dialog.BACKUP_DELETED -> { - DeletingBackupDialog( - backupDeleted = requestedDialog == RemoteBackupsSettingsState.Dialog.BACKUP_DELETED, - onDismiss = contentCallbacks::onDialogDismissed - ) + RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER -> { + CircularProgressDialog(onDismiss = contentCallbacks::onDialogDismissed) + } + + RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> { + DownloadingYourBackupDialog(onDismiss = contentCallbacks::onDialogDismissed) } } @@ -502,11 +529,11 @@ private fun LazyListScope.appendBackupDetailsItems( @Composable private fun BackupCard( - messageBackupsType: MessageBackupsType, - backupState: RemoteBackupsSettingsState.BackupState, - renewalTime: Duration, + backupState: RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime, onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {} ) { + val messageBackupsType = backupState.messageBackupsType + Column( modifier = Modifier .fillMaxWidth() @@ -523,7 +550,7 @@ private fun BackupCard( Text( text = buildAnnotatedString { - if (backupState == RemoteBackupsSettingsState.BackupState.ACTIVE) { + if (backupState.isActive()) { SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK) append(" ") } @@ -535,28 +562,35 @@ private fun BackupCard( ) when (backupState) { - RemoteBackupsSettingsState.BackupState.ACTIVE -> { - val cost = when (messageBackupsType) { - is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth)) - is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free) - } - + is RemoteBackupsSettingsState.BackupState.ActivePaid -> { Text( - text = cost, + text = stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, backupState.price)), modifier = Modifier.padding(top = 12.dp) ) } - RemoteBackupsSettingsState.BackupState.INACTIVE -> { + is RemoteBackupsSettingsState.BackupState.ActiveFree -> { Text( - text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_inactive), + text = stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free), + modifier = Modifier.padding(top = 12.dp) + ) + } + + is RemoteBackupsSettingsState.BackupState.Inactive -> { + val text = when (messageBackupsType) { + is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__subscription_inactive) + is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__you_turned_off_backups) + } + + Text( + text = text, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp) ) } - RemoteBackupsSettingsState.BackupState.CANCELED -> { + is RemoteBackupsSettingsState.BackupState.Canceled -> { Text( text = stringResource(R.string.RemoteBackupsSettingsFragment__subscription_cancelled), color = MaterialTheme.colorScheme.error, @@ -565,21 +599,20 @@ private fun BackupCard( ) } - else -> error("Not supported here.") + else -> error("Not supported here: $backupState") } if (messageBackupsType is MessageBackupsType.Paid) { - @Suppress("KotlinConstantConditions") val resource = when (backupState) { - RemoteBackupsSettingsState.BackupState.ACTIVE -> R.string.RemoteBackupsSettingsFragment__renews_s - RemoteBackupsSettingsState.BackupState.INACTIVE -> R.string.RemoteBackupsSettingsFragment__expired_on_s - RemoteBackupsSettingsState.BackupState.CANCELED -> R.string.RemoteBackupsSettingsFragment__expires_on_s + is RemoteBackupsSettingsState.BackupState.ActivePaid -> R.string.RemoteBackupsSettingsFragment__renews_s + is RemoteBackupsSettingsState.BackupState.Inactive -> R.string.RemoteBackupsSettingsFragment__expired_on_s + is RemoteBackupsSettingsState.BackupState.Canceled -> R.string.RemoteBackupsSettingsFragment__expires_on_s else -> error("Not supported here.") } - if (renewalTime > 0.seconds) { + if (backupState.renewalTime > 0.seconds) { Text( - text = stringResource(resource, DateUtils.formatDateWithYear(Locale.getDefault(), renewalTime.inWholeMilliseconds)) + text = stringResource(resource, DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds)) ) } } @@ -599,7 +632,7 @@ private fun BackupCard( is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__upgrade) } - if (backupState == RemoteBackupsSettingsState.BackupState.ACTIVE) { + if (backupState.isActive()) { Buttons.LargeTonal( onClick = { onBackupTypeActionButtonClicked(messageBackupsType.tier) }, colors = ButtonDefaults.filledTonalButtonColors().copy( @@ -641,7 +674,56 @@ private fun LoadingCard() { @Composable private fun ErrorCard() { BoxCard { - Text(text = "Error") // TODO [alex] -- Finalized error card + Column { + CircularProgressIndicator( + strokeWidth = 3.5.dp, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(32.dp) + ) + + Text(text = stringResource(R.string.RemoteBackupsSettingsFragment__waiting_for_network)) + } + } +} + +@Composable +private fun PendingCard( + price: FiatMoney +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp) + .background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp)) + .padding(24.dp) + ) { + Text( + text = stringResource(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 12.dp) + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.RemoteBackupsSettingsFragment__s_per_month, FiatMoneyUtil.format(LocalContext.current.resources, price)) + ) + Text( + text = stringResource(R.string.RemoteBackupsSettingsFragment__payment_pending) + ) + } + + CircularProgressIndicator( + strokeWidth = 3.5.dp, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(32.dp) + ) + } } } @@ -755,10 +837,22 @@ private fun TurnOffAndDeleteBackupsDialog( ) } +@Composable +private fun DownloadingYourBackupDialog( + onDismiss: () -> Unit +) { + Dialogs.SimpleAlertDialog( + title = stringResource(R.string.RemoteBackupsSettingsFragment__downloading_your_backup), + body = stringResource(R.string.RemoteBackupsSettingsFragment__depending_on_the_size), + confirm = stringResource(android.R.string.ok), + onConfirm = {}, + onDismiss = onDismiss + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun DeletingBackupDialog( - backupDeleted: Boolean, +private fun CircularProgressDialog( onDismiss: () -> Unit ) { BasicAlertDialog( @@ -772,38 +866,14 @@ private fun DeletingBackupDialog( shape = AlertDialogDefaults.shape, color = AlertDialogDefaults.containerColor ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .defaultMinSize(minWidth = 232.dp) - .padding(bottom = 60.dp) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.aspectRatio(1f) ) { - if (backupDeleted) { - Icon( - painter = painterResource(id = R.drawable.symbol_check_light_24), - contentDescription = null, - tint = Color(0xFF09B37B), - modifier = Modifier - .padding(top = 58.dp, bottom = 9.dp) - .size(48.dp) - ) - Text( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_deleted), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else { - CircularProgressIndicator( - modifier = Modifier - .padding(top = 64.dp, bottom = 20.dp) - .size(48.dp) - ) - Text( - text = stringResource(id = R.string.RemoteBackupsSettingsFragment__deleting_backup), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + CircularProgressIndicator( + modifier = Modifier + .size(48.dp) + ) } } } @@ -882,8 +952,7 @@ private fun getTextForFrequency(backupsFrequency: BackupFrequency): String { private fun RemoteBackupsSettingsContentPreview() { Previews.Preview { RemoteBackupsSettingsContent( - backupsInitialized = true, - messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30), + backupsEnabled = true, lastBackupTimestamp = -1, canBackUpUsingCellular = false, backupsFrequency = BackupFrequency.MANUAL, @@ -891,9 +960,11 @@ private fun RemoteBackupsSettingsContentPreview() { requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE, contentCallbacks = object : ContentCallbacks {}, backupProgress = null, - renewalTime = 1727193018.seconds, backupSize = 2300000, - backupState = RemoteBackupsSettingsState.BackupState.ACTIVE + backupState = RemoteBackupsSettingsState.BackupState.ActiveFree( + messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30) + ), + backupRestoreState = BackupRestoreState(false, BackupStatusData.CouldNotCompleteBackup) ) } } @@ -914,44 +985,69 @@ private fun ErrorCardPreview() { } } +@SignalPreview +@Composable +private fun PendingCardPreview() { + Previews.Preview { + PendingCard( + price = FiatMoney(BigDecimal.TEN, Currency.getInstance(Locale.getDefault())) + ) + } +} + @SignalPreview @Composable private fun BackupCardPreview() { Previews.Preview { Column { BackupCard( - messageBackupsType = MessageBackupsType.Paid( - pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), - storageAllowanceBytes = 100_000_000 - ), - backupState = RemoteBackupsSettingsState.BackupState.ACTIVE, - renewalTime = 1727193018.seconds + backupState = RemoteBackupsSettingsState.BackupState.ActivePaid( + messageBackupsType = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), + storageAllowanceBytes = 100_000_000 + ), + renewalTime = 1727193018.seconds, + price = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")) + ) ) BackupCard( - messageBackupsType = MessageBackupsType.Paid( - pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), - storageAllowanceBytes = 100_000_000 - ), - backupState = RemoteBackupsSettingsState.BackupState.CANCELED, - renewalTime = 1727193018.seconds + backupState = RemoteBackupsSettingsState.BackupState.Canceled( + messageBackupsType = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), + storageAllowanceBytes = 100_000_000 + ), + renewalTime = 1727193018.seconds + ) ) BackupCard( - messageBackupsType = MessageBackupsType.Paid( - pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), - storageAllowanceBytes = 100_000_000 - ), - backupState = RemoteBackupsSettingsState.BackupState.INACTIVE, - renewalTime = 1727193018.seconds + backupState = RemoteBackupsSettingsState.BackupState.Inactive( + messageBackupsType = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), + storageAllowanceBytes = 100_000_000 + ), + renewalTime = 1727193018.seconds + ) ) BackupCard( - messageBackupsType = MessageBackupsType.Free( - mediaRetentionDays = 30 - ), - backupState = RemoteBackupsSettingsState.BackupState.ACTIVE, - renewalTime = 0.seconds + backupState = RemoteBackupsSettingsState.BackupState.ActivePaid( + messageBackupsType = MessageBackupsType.Paid( + pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")), + storageAllowanceBytes = 100_000_000 + ), + renewalTime = 1727193018.seconds, + price = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")) + ) + ) + + BackupCard( + backupState = RemoteBackupsSettingsState.BackupState.ActiveFree( + messageBackupsType = MessageBackupsType.Free( + mediaRetentionDays = 30 + ) + ) ) } } @@ -999,10 +1095,19 @@ private fun TurnOffAndDeleteBackupsDialogPreview() { @SignalPreview @Composable -private fun DeleteBackupDialogPreview() { +private fun DownloadingYourBackupDialogPreview() { Previews.Preview { - DeletingBackupDialog( - backupDeleted = true, + DownloadingYourBackupDialog( + onDismiss = {} + ) + } +} + +@SignalPreview +@Composable +private fun CircularProgressDialogPreview() { + Previews.Preview { + CircularProgressDialog( onDismiss = {} ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt index 446cfd20db..c890f5072d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt @@ -5,59 +5,104 @@ package org.thoughtcrime.securesms.components.settings.app.backups.remote +import org.signal.core.util.money.FiatMoney import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds data class RemoteBackupsSettingsState( - val backupsInitialized: Boolean, - val messageBackupsType: MessageBackupsType? = null, + val backupsEnabled: Boolean, val canBackUpUsingCellular: Boolean = false, - val backupState: BackupState = BackupState.LOADING, + val backupState: BackupState = BackupState.Loading, val backupSize: Long = 0, val backupsFrequency: BackupFrequency = BackupFrequency.DAILY, val lastBackupTimestamp: Long = 0, - val renewalTime: Duration = 0.seconds, val dialog: Dialog = Dialog.NONE, val snackbar: Snackbar = Snackbar.NONE ) { + /** * Describes the state of the user's selected backup tier. */ - enum class BackupState { + sealed interface BackupState { + + /** + * User has no active backup tier, no tier history + */ + data object None : BackupState + /** * The exact backup state is being loaded from the network. */ - LOADING, + data object Loading : BackupState /** - * User has an active backup + * User has a paid backup subscription pending redemption */ - ACTIVE, + data class Pending( + val price: FiatMoney + ) : BackupState /** - * User has an inactive paid tier backup + * A backup state with a type and renewal time */ - INACTIVE, + sealed interface WithTypeAndRenewalTime : BackupState { + val messageBackupsType: MessageBackupsType + val renewalTime: Duration + + fun isActive(): Boolean = false + } + + /** + * User has an active paid backup. Pricing comes from the subscription object. + */ + data class ActivePaid( + override val messageBackupsType: MessageBackupsType.Paid, + val price: FiatMoney, + override val renewalTime: Duration + ) : WithTypeAndRenewalTime { + override fun isActive(): Boolean = true + } + + /** + * User has an active free backup. + */ + data class ActiveFree( + override val messageBackupsType: MessageBackupsType.Free, + override val renewalTime: Duration = 0.seconds + ) : WithTypeAndRenewalTime { + override fun isActive(): Boolean = true + } + + /** + * User has an inactive backup + */ + data class Inactive( + override val messageBackupsType: MessageBackupsType, + override val renewalTime: Duration = 0.seconds + ) : WithTypeAndRenewalTime /** * User has a canceled paid tier backup */ - CANCELED, + data class Canceled( + override val messageBackupsType: MessageBackupsType, + override val renewalTime: Duration + ) : WithTypeAndRenewalTime /** * An error occurred retrieving the network state */ - ERROR + data object Error : BackupState } enum class Dialog { NONE, TURN_OFF_AND_DELETE_BACKUPS, BACKUP_FREQUENCY, - DELETING_BACKUP, - BACKUP_DELETED, + PROGRESS_SPINNER, + DOWNLOADING_YOUR_BACKUP, TURN_OFF_FAILED } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 815f3e24fe..17742eaa21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -8,29 +8,45 @@ package org.thoughtcrime.securesms.components.settings.app.backups.remote import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.withContext import org.signal.core.util.logging.Log +import org.signal.core.util.money.FiatMoney +import org.signal.donations.InAppPaymentType import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData +import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType +import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner +import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney +import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository +import org.thoughtcrime.securesms.database.InAppPaymentTable +import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobs.BackupMessagesJob +import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.service.MessageBackupListener -import kotlin.time.Duration.Companion.milliseconds +import java.util.Currency import kotlin.time.Duration.Companion.seconds /** * ViewModel for state management of RemoteBackupsSettingsFragment */ +@OptIn(ExperimentalCoroutinesApi::class) class RemoteBackupsSettingsViewModel : ViewModel() { companion object { @@ -39,8 +55,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { private val _state = MutableStateFlow( RemoteBackupsSettingsState( - backupsInitialized = SignalStore.backup.backupsInitialized, - messageBackupsType = null, + backupsEnabled = SignalStore.backup.areBackupsEnabled, lastBackupTimestamp = SignalStore.backup.lastBackupTime, backupSize = SignalStore.backup.totalBackupSize, backupsFrequency = SignalStore.backup.backupFrequency, @@ -48,7 +63,36 @@ class RemoteBackupsSettingsViewModel : ViewModel() { ) ) + private val _restoreState: MutableStateFlow = MutableStateFlow(BackupRestoreState(false, BackupStatusData.RestoringMedia())) + private val latestPurchaseId = MutableSharedFlow() + val state: StateFlow = _state + val restoreState: StateFlow = _restoreState + + init { + viewModelScope.launch(Dispatchers.IO) { + latestPurchaseId + .flatMapLatest { id -> InAppPaymentsRepository.observeUpdates(id).asFlow() } + .collectLatest { purchase -> + refreshState(purchase) + } + } + + viewModelScope.launch(Dispatchers.Default) { + val restoreProgress = MediaRestoreProgressBanner() + + while (isActive) { + if (restoreProgress.enabled) { + Log.d(TAG, "Backup is being restored. Collecting updates.") + restoreProgress.dataFlow.collectLatest { latest -> + _restoreState.update { BackupRestoreState(restoreProgress.enabled, latest) } + } + } + + delay(1.seconds) + } + } + } fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) { SignalStore.backup.backupWithCellular = canBackUpUsingCellular @@ -71,104 +115,147 @@ class RemoteBackupsSettingsViewModel : ViewModel() { } fun refresh() { - viewModelScope.launch { - Log.d(TAG, "Attempting to synchronize backup tier from archive service.") + viewModelScope.launch(Dispatchers.IO) { + val id = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)?.id - val backupTier = withContext(Dispatchers.IO) { - BackupRepository.getBackupTier() + if (id != null) { + latestPurchaseId.emit(id) + } else { + refreshState(null) } + } + } - backupTier.runIfSuccessful { - Log.d(TAG, "Setting backup tier to $it") - SignalStore.backup.backupTier = it - } + private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) { + val tier = SignalStore.backup.latestBackupTier - val tier = SignalStore.backup.backupTier - val backupType = if (tier != null) BackupRepository.getBackupsType(tier) else null + _state.update { + it.copy( + backupsEnabled = SignalStore.backup.areBackupsEnabled, + backupState = RemoteBackupsSettingsState.BackupState.Loading, + lastBackupTimestamp = SignalStore.backup.lastBackupTime, + backupSize = SignalStore.backup.totalBackupSize, + backupsFrequency = SignalStore.backup.backupFrequency, + canBackUpUsingCellular = SignalStore.backup.backupWithCellular + ) + } + if (lastPurchase?.state == InAppPaymentTable.State.PENDING) { + Log.d(TAG, "We have a pending subscription.") _state.update { it.copy( - backupsInitialized = SignalStore.backup.backupsInitialized, - messageBackupsType = backupType, - backupState = RemoteBackupsSettingsState.BackupState.LOADING, - lastBackupTimestamp = SignalStore.backup.lastBackupTime, - backupSize = SignalStore.backup.totalBackupSize, - backupsFrequency = SignalStore.backup.backupFrequency, - canBackUpUsingCellular = SignalStore.backup.backupWithCellular + backupState = RemoteBackupsSettingsState.BackupState.Pending( + price = lastPurchase.data.amount!!.toFiatMoney() + ) ) } - when (tier) { - MessageBackupTier.PAID -> { - Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.") + return + } - val activeSubscription = withContext(Dispatchers.IO) { - RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) - } + when (tier) { + MessageBackupTier.PAID -> { + Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.") - if (activeSubscription.isSuccess) { - Log.d(TAG, "Retrieved subscription details.") + val type = withContext(Dispatchers.IO) { + BackupRepository.getBackupsType(tier) as MessageBackupsType.Paid + } - val subscription = activeSubscription.getOrThrow().activeSubscription - if (subscription != null) { - Log.d(TAG, "Subscription found. Updating UI state with subscription details.") - _state.update { - it.copy( - renewalTime = subscription.endOfCurrentPeriod.seconds, - backupState = when { - subscription.isActive -> RemoteBackupsSettingsState.BackupState.ACTIVE - subscription.isCanceled -> RemoteBackupsSettingsState.BackupState.CANCELED - else -> RemoteBackupsSettingsState.BackupState.INACTIVE - } - ) - } - } else { - Log.d(TAG, "ActiveSubscription had null subscription object. Updating UI state with INACTIVE subscription.") - _state.update { - it.copy( - renewalTime = 0.seconds, - backupState = RemoteBackupsSettingsState.BackupState.INACTIVE - ) - } - } - } else { - Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.") + val activeSubscription = withContext(Dispatchers.IO) { + RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) + } + + if (activeSubscription.isSuccess) { + Log.d(TAG, "Retrieved subscription details.") + + val subscription = activeSubscription.getOrThrow().activeSubscription + if (subscription != null) { + Log.d(TAG, "Subscription found. Updating UI state with subscription details.") _state.update { it.copy( - renewalTime = 0.seconds, - backupState = RemoteBackupsSettingsState.BackupState.ERROR + backupState = when { + subscription.isActive -> RemoteBackupsSettingsState.BackupState.ActivePaid( + messageBackupsType = type, + price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)), + renewalTime = subscription.endOfCurrentPeriod.seconds + ) + subscription.isCanceled -> RemoteBackupsSettingsState.BackupState.Canceled( + messageBackupsType = type, + renewalTime = subscription.endOfCurrentPeriod.seconds + ) + else -> RemoteBackupsSettingsState.BackupState.Inactive( + messageBackupsType = type, + renewalTime = subscription.endOfCurrentPeriod.seconds + ) + } + ) + } + } else { + Log.d(TAG, "ActiveSubscription had null subscription object. Updating UI state with INACTIVE subscription.") + _state.update { + it.copy( + backupState = RemoteBackupsSettingsState.BackupState.Inactive(type) ) } } + } else { + Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.") + _state.update { + it.copy( + backupState = RemoteBackupsSettingsState.BackupState.Error + ) + } + } + } + + MessageBackupTier.FREE -> { + val type = withContext(Dispatchers.IO) { + BackupRepository.getBackupsType(tier) as MessageBackupsType.Free } - MessageBackupTier.FREE -> { - Log.d(TAG, "Updating UI state with ACTIVE FREE tier.") - _state.update { it.copy(renewalTime = 0.seconds, backupState = RemoteBackupsSettingsState.BackupState.ACTIVE) } - } - null -> { - Log.d(TAG, "Updating UI state with INACTIVE null tier.") - _state.update { it.copy(renewalTime = 0.seconds, backupState = RemoteBackupsSettingsState.BackupState.INACTIVE) } + val backupState = if (SignalStore.backup.areBackupsEnabled) { + RemoteBackupsSettingsState.BackupState.ActiveFree(type) + } else { + RemoteBackupsSettingsState.BackupState.Inactive(type) } + + Log.d(TAG, "Updating UI state with $backupState FREE tier.") + _state.update { it.copy(backupState = backupState) } + } + + null -> { + Log.d(TAG, "Updating UI state with NONE null tier.") + _state.update { it.copy(backupState = RemoteBackupsSettingsState.BackupState.None) } } } } fun turnOffAndDeleteBackups() { viewModelScope.launch { - requestDialog(RemoteBackupsSettingsState.Dialog.DELETING_BACKUP) + Log.d(TAG, "Beginning to turn off and delete backup.") + requestDialog(RemoteBackupsSettingsState.Dialog.PROGRESS_SPINNER) + + val hasMediaBackupUploaded = SignalStore.backup.backsUpMedia && SignalStore.backup.hasBackupBeenUploaded val succeeded = withContext(Dispatchers.IO) { - BackupRepository.turnOffAndDeleteBackup() + BackupRepository.turnOffAndDisableBackups() } if (isActive) { if (succeeded) { - requestDialog(RemoteBackupsSettingsState.Dialog.BACKUP_DELETED) - delay(2000.milliseconds) - requestDialog(RemoteBackupsSettingsState.Dialog.NONE) + if (hasMediaBackupUploaded && SignalStore.backup.optimizeStorage) { + Log.d(TAG, "User has optimized storage, downloading.") + requestDialog(RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP) + + SignalStore.backup.optimizeStorage = false + RestoreOptimizedMediaJob.enqueue() + } else { + Log.d(TAG, "User does not have optimized storage, finished.") + requestDialog(RemoteBackupsSettingsState.Dialog.NONE) + } refresh() } else { + Log.d(TAG, "Failed to disable backups.") requestDialog(RemoteBackupsSettingsState.Dialog.TURN_OFF_FAILED) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt index 11a81978e2..a0b005d13e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt @@ -99,6 +99,9 @@ object InAppPaymentsRepository { val observer = InAppPaymentObserver { refresh() } + + refresh() + AppDependencies.databaseObserver.registerInAppPaymentObserver(observer) awaitClose { AppDependencies.databaseObserver.unregisterObserver(observer) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt index 758128420b..e37cfdfe4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -9,7 +9,6 @@ import androidx.annotation.VisibleForTesting import org.signal.core.util.billing.BillingPurchaseResult import org.signal.core.util.logging.Log import org.signal.donations.InAppPaymentType -import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository @@ -66,8 +65,8 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C return Result.success() } - if (!SignalStore.backup.backupsInitialized) { - Log.i(TAG, "Backups are not initialized on this device. Exiting.") + if (!SignalStore.backup.areBackupsEnabled) { + Log.i(TAG, "Backups are not enabled on this device. Exiting.") return Result.success() } @@ -76,11 +75,6 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C return Result.success() } - BackupRepository.getBackupTier().runIfSuccessful { - Log.i(TAG, "Successfully retrieved backup tier $it. Applying.") - SignalStore.backup.backupTier = it - } - val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases() val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth() diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 7bfd947991..e30696492d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -25,6 +25,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_BACKUP_USED_MEDIA_SPACE = "backup.usedMediaSpace" private const val KEY_BACKUP_LAST_PROTO_SIZE = "backup.lastProtoSize" private const val KEY_BACKUP_TIER = "backup.backupTier" + private const val KEY_LATEST_BACKUP_TIER = "backup.latestBackupTier" private const val KEY_NEXT_BACKUP_TIME = "backup.nextBackupTime" private const val KEY_LAST_BACKUP_TIME = "backup.lastBackupTime" @@ -64,7 +65,32 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { var lastBackupTime: Long by longValue(KEY_LAST_BACKUP_TIME, -1) var lastMediaSyncTime: Long by longValue(KEY_LAST_BACKUP_MEDIA_SYNC_TIME, -1) var backupFrequency: BackupFrequency by enumValue(KEY_BACKUP_FREQUENCY, BackupFrequency.MANUAL, BackupFrequency.Serializer) - var backupTier: MessageBackupTier? by enumValue(KEY_BACKUP_TIER, null, MessageBackupTier.Serializer) + + /** + * This is the 'latest' backup tier. This isn't necessarily the user's current backup tier, so this should only ever + * be used to display backup tier information to the user in the settings fragments, not to check whether the user + * currently has backups enabled. + */ + val latestBackupTier: MessageBackupTier? by enumValue(KEY_LATEST_BACKUP_TIER, null, MessageBackupTier.Serializer) + + /** + * When seting the backup tier, we also want to write to the latestBackupTier, as long as + * the value is non-null. This gives us a 1-deep history of the selected backup tier for + * use in the UI + */ + var backupTier: MessageBackupTier? + get() = MessageBackupTier.deserialize(getLong(KEY_BACKUP_TIER, -1)) + set(value) { + val serializedValue = MessageBackupTier.serialize(value) + if (value != null) { + store.beginWrite() + .putLong(KEY_BACKUP_TIER, serializedValue) + .putLong(KEY_LATEST_BACKUP_TIER, serializedValue) + .apply() + } else { + putLong(KEY_BACKUP_TIER, serializedValue) + } + } /** * When uploading a backup, we store the progress state here so that I can remain across app restarts. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dc81d47347..d1d21acc5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7587,12 +7587,18 @@ Backup will be created overnight. Subscription inactive + + You turned off backups Backup plan Backups disabled %1$s/month + + Payment pending… + + Waiting for network… Your backup plan is free @@ -7649,6 +7655,10 @@ Re-enable backups Backups have been turned off and your data has been deleted from Signal\'s secure storage service. + + Downloading your backup + + Depending on the size of your backup, this could take a long time. You can use your phone as you normally do while the download takes place.