Add subscription states to the remote backups settings.
This commit is contained in:
parent
a66c7058b1
commit
b519bf6772
12 changed files with 522 additions and 227 deletions
|
@ -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<MessageBackupTier> {
|
||||
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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<BackupStatusData>() {
|
||||
class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerListener = EmptyListener) : Banner<BackupStatusData>() {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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<BackupRestoreState> = MutableStateFlow(BackupRestoreState(false, BackupStatusData.RestoringMedia()))
|
||||
private val latestPurchaseId = MutableSharedFlow<InAppPaymentTable.InAppPaymentId>()
|
||||
|
||||
val state: StateFlow<RemoteBackupsSettingsState> = _state
|
||||
val restoreState: StateFlow<BackupRestoreState> = _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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,9 @@ object InAppPaymentsRepository {
|
|||
val observer = InAppPaymentObserver {
|
||||
refresh()
|
||||
}
|
||||
|
||||
refresh()
|
||||
|
||||
AppDependencies.databaseObserver.registerInAppPaymentObserver(observer)
|
||||
awaitClose {
|
||||
AppDependencies.databaseObserver.unregisterObserver(observer)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -7587,12 +7587,18 @@
|
|||
<string name="RemoteBackupsSettingsFragment__backup_will_be_created_overnight">Backup will be created overnight.</string>
|
||||
<!-- Text displayed in card when subscription is inactive -->
|
||||
<string name="RemoteBackupsSettingsFragment__subscription_inactive">Subscription inactive</string>
|
||||
<!-- Text displayed in card when free tier is inactive -->
|
||||
<string name="RemoteBackupsSettingsFragment__you_turned_off_backups">You turned off backups</string>
|
||||
<!-- Title text in row detailing selected backup type -->
|
||||
<string name="RemoteBackupsSettingsFragment__backup_plan">Backup plan</string>
|
||||
<!-- Subtitle text in row detailing selected backup type displayed when backups are disabled -->
|
||||
<string name="RemoteBackupsSettingsFragment__backups_disabled">Backups disabled</string>
|
||||
<!-- Format string for backup and cost. First placeholder is cost per month. -->
|
||||
<string name="RemoteBackupsSettingsFragment__s_per_month">%1$s/month</string>
|
||||
<!-- Displayed in card at top of screen while payment processing is pending -->
|
||||
<string name="RemoteBackupsSettingsFragment__payment_pending">Payment pending…</string>
|
||||
<!-- Displayed in card while waiting for network -->
|
||||
<string name="RemoteBackupsSettingsFragment__waiting_for_network">Waiting for network…</string>
|
||||
<!-- String for free backups -->
|
||||
<string name="RemoteBackupsSettingsFragment__your_backup_plan_is_free">Your backup plan is free</string>
|
||||
<!-- Displays the date the subscription will renew. Placeholder is the date. -->
|
||||
|
@ -7649,6 +7655,10 @@
|
|||
<string name="RemoteBackupsSettingsFragment__reenable_backups">Re-enable backups</string>
|
||||
<!-- Notice displayed when backups have been disabled and deleted -->
|
||||
<string name="RemoteBackupsSettingsFragment__backups_have_been_turned_off">Backups have been turned off and your data has been deleted from Signal\'s secure storage service.</string>
|
||||
<!-- Dialog title for dialog which alerts user their optimized media will be downloaded -->
|
||||
<string name="RemoteBackupsSettingsFragment__downloading_your_backup">Downloading your backup</string>
|
||||
<!-- Dialog message for dialog which alerts user their optimized media will be downloaded -->
|
||||
<string name="RemoteBackupsSettingsFragment__depending_on_the_size">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.</string>
|
||||
|
||||
<!-- MessageBackupsEducationScreen -->
|
||||
<!-- Screen subtitle underneath large headline title -->
|
||||
|
|
Loading…
Add table
Reference in a new issue