diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupState.kt index 6eebc46fa5..70c9bc0cb7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupState.kt @@ -6,6 +6,7 @@ package org.thoughtcrime.securesms.components.settings.app.backups import org.signal.core.util.money.FiatMoney +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -25,9 +26,9 @@ sealed interface BackupState { data object None : BackupState /** - * The exact backup state is being loaded from the network. + * Temporary state object that just denotes what the local store thinks we are. */ - data object Loading : BackupState + data class LocalStore(val tier: MessageBackupTier) : BackupState /** * User has a paid backup subscription pending redemption diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt similarity index 57% rename from app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateRepository.kt rename to app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt index e0aa1f84c2..b827e5b6ff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt @@ -5,20 +5,35 @@ package org.thoughtcrime.securesms.components.settings.app.backups +import androidx.annotation.WorkerThread +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlow import kotlinx.coroutines.withContext import org.signal.core.util.billing.BillingPurchaseResult +import org.signal.core.util.concurrent.SignalDispatchers import org.signal.core.util.logging.Log import org.signal.core.util.money.FiatMoney +import org.signal.core.util.throttleLatest +import org.signal.donations.InAppPaymentType import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType 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.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.InternetConnectionObserver +import org.thoughtcrime.securesms.util.RemoteConfig import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription import java.math.BigDecimal @@ -30,12 +45,211 @@ import kotlin.time.Duration.Companion.seconds /** * Manages BackupState information gathering for the UI. + * + * This class utilizes a stream of requests which are throttled to one per 100ms, such that we don't flood + * ourselves with network and database activity. + * + * @param scope A coroutine scope, generally expected to be a viewModelScope + * @param useDatabaseFallbackOnNetworkError Whether we will display network errors or fall back to database information. Defaults to false. */ -object BackupStateRepository { +class BackupStateObserver( + scope: CoroutineScope, + private val useDatabaseFallbackOnNetworkError: Boolean = false +) { + companion object { + private val TAG = Log.tag(BackupStateObserver::class) - private val TAG = Log.tag(BackupStateRepository::class) + private val staticScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + private val backupTierChangedNotifier = MutableSharedFlow() - suspend fun resolveBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState { + /** + * Called when the value returned by [SignalStore.backup.backupTier] changes. + */ + fun notifyBackupTierChanged(scope: CoroutineScope = staticScope) { + Log.d(TAG, "Notifier got a change") + scope.launch { + backupTierChangedNotifier.emit(Unit) + } + } + + /** + * Builds a BackupState without touching the database or network. At most what this + * can tell you is whether the tier is set or if backups are available at all. + * + * This method is meant to be lightweight and instantaneous, and is a good candidate for + * setting initial ViewModel state values. + */ + fun getNonIOBackupState(): BackupState { + return if (RemoteConfig.messageBackups) { + val tier = SignalStore.backup.backupTier + + if (tier != null) { + BackupState.LocalStore(tier) + } else { + BackupState.None + } + } else { + BackupState.NotAvailable + } + } + } + + private val internalBackupState = MutableStateFlow(getNonIOBackupState()) + private val backupStateRefreshRequest = MutableSharedFlow() + + val backupState: StateFlow = internalBackupState + + init { + scope.launch(SignalDispatchers.IO) { + performDatabaseBackupStateRefresh() + } + + scope.launch(SignalDispatchers.IO) { + backupStateRefreshRequest + .throttleLatest(100.milliseconds) + .collect { + performFullBackupStateRefresh() + } + } + + scope.launch(SignalDispatchers.IO) { + backupTierChangedNotifier.collect { + requestBackupStateRefresh() + } + } + + scope.launch(SignalDispatchers.IO) { + InternetConnectionObserver.observe().asFlow() + .collect { + if (backupState.value == BackupState.Error) { + requestBackupStateRefresh() + } + } + } + + scope.launch(SignalDispatchers.IO) { + InAppPaymentsRepository.observeLatestBackupPayment().collect { + requestBackupStateRefresh() + } + } + + scope.launch(SignalDispatchers.IO) { + SignalStore.backup.subscriptionStateMismatchDetectedFlow.collect { + requestBackupStateRefresh() + } + } + + scope.launch(SignalDispatchers.IO) { + SignalStore.backup.deletionStateFlow.collect { + requestBackupStateRefresh() + } + } + } + + /** + * Requests a refresh behind a throttler. + */ + private suspend fun requestBackupStateRefresh() { + Log.d(TAG, "Requesting refresh.") + backupStateRefreshRequest.emit(Unit) + } + + /** + * Produces state based off what we have locally in the database. Does not hit the network. + */ + @WorkerThread + private fun getDatabaseBackupState(): BackupState { + if (SignalStore.backup.backupTier != MessageBackupTier.PAID) { + Log.d(TAG, "No additional information available without accessing the network.") + return getNonIOBackupState() + } + + val latestPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) + if (latestPayment == null) { + Log.d(TAG, "No additional information is available in the local database.") + return getNonIOBackupState() + } + + val price = latestPayment.data.amount!!.toFiatMoney() + val isPending = SignalDatabase.inAppPayments.hasPendingBackupRedemption() + if (isPending) { + return BackupState.Pending(price = price) + } + + val paidBackupType = MessageBackupsType.Paid( + pricePerMonth = price, + storageAllowanceBytes = -1L, + mediaTtl = 0.days + ) + + val isCanceled = latestPayment.data.cancellation != null + if (isCanceled) { + return BackupState.Canceled( + messageBackupsType = paidBackupType, + renewalTime = latestPayment.endOfPeriod + ) + } + + if (SignalStore.backup.subscriptionStateMismatchDetected) { + return BackupState.SubscriptionMismatchMissingGooglePlay( + messageBackupsType = paidBackupType, + renewalTime = latestPayment.endOfPeriod + ) + } + + if (latestPayment.endOfPeriod < System.currentTimeMillis().milliseconds) { + return BackupState.Inactive( + messageBackupsType = paidBackupType, + renewalTime = latestPayment.endOfPeriod + ) + } + + return BackupState.ActivePaid( + messageBackupsType = paidBackupType, + price = price, + renewalTime = latestPayment.endOfPeriod + ) + } + + private suspend fun performDatabaseBackupStateRefresh() { + if (!RemoteConfig.messageBackups) { + return + } + + if (!SignalStore.account.isRegistered) { + Log.d(TAG, "Dropping refresh for unregistered user.") + return + } + + if (backupState.value !is BackupState.LocalStore) { + Log.d(TAG, "Dropping database refresh for non-local store state.") + return + } + + internalBackupState.emit(getDatabaseBackupState()) + } + + private suspend fun performFullBackupStateRefresh() { + if (!RemoteConfig.messageBackups) { + return + } + + if (!SignalStore.account.isRegistered) { + Log.d(TAG, "Dropping refresh for unregistered user.") + return + } + + Log.d(TAG, "Performing refresh.") + withContext(SignalDispatchers.IO) { + val latestInAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) + internalBackupState.emit(getNetworkBackupState(latestInAppPayment)) + } + } + + /** + * Utilizes everything we can to resolve the most accurate backup state available, including database and network. + */ + private suspend fun getNetworkBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState { if (lastPurchase?.state == InAppPaymentTable.State.PENDING) { Log.d(TAG, "We have a pending subscription.") return BackupState.Pending( @@ -74,7 +288,7 @@ object BackupStateRepository { if (type == null) { Log.d(TAG, "[subscriptionMismatchDetected] failed to load backup configuration. Likely a network error.") - return BackupState.Error + return getStateOnError() } return BackupState.SubscriptionMismatchMissingGooglePlay( @@ -116,6 +330,17 @@ object BackupStateRepository { } } + /** + * Helper function to fall back to database state if [useDatabaseFallbackOnNetworkError] is set to true. + */ + private fun getStateOnError(): BackupState { + return if (useDatabaseFallbackOnNetworkError) { + getDatabaseBackupState() + } else { + BackupState.Error + } + } + private suspend fun getPaidBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState { Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.") @@ -141,7 +366,7 @@ object BackupStateRepository { if (subscriberType == null) { Log.d(TAG, "Failed to create backup type. Possible network error.") - BackupState.Error + getStateOnError() } else { when { subscription.isCanceled && subscription.isActive -> BackupState.Canceled( @@ -169,7 +394,7 @@ object BackupStateRepository { val canceledType = type ?: buildPaidTypeFromInAppPayment(lastPurchase) if (canceledType == null) { Log.w(TAG, "Failed to load canceled type information. Possible network error.") - BackupState.Error + getStateOnError() } else { BackupState.Canceled( messageBackupsType = canceledType, @@ -180,7 +405,7 @@ object BackupStateRepository { val inactiveType = type ?: buildPaidTypeWithoutPricing() if (inactiveType == null) { Log.w(TAG, "Failed to load inactive type information. Possible network error.") - BackupState.Error + getStateOnError() } else { BackupState.Inactive( messageBackupsType = inactiveType, @@ -191,7 +416,7 @@ object BackupStateRepository { } } else { Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.") - BackupState.Error + getStateOnError() } } @@ -202,7 +427,7 @@ object BackupStateRepository { if (type !is NetworkResult.Success) { Log.w(TAG, "Failed to load FREE type.", type.getCause()) - return BackupState.Error + return getStateOnError() } val backupState = if (SignalStore.backup.areBackupsEnabled) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt index c5b5880190..b3cb56b48b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsFragment.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -80,11 +79,6 @@ class BackupsSettingsFragment : ComposeFragment() { } } - override fun onResume() { - super.onResume() - viewModel.refreshState() - } - @Composable override fun FragmentContent() { val state by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -94,7 +88,7 @@ class BackupsSettingsFragment : ComposeFragment() { onNavigationClick = { requireActivity().onNavigateUp() }, onBackupsRowClick = { when (state.backupState) { - BackupState.Loading, BackupState.Error, BackupState.NotAvailable -> Unit + BackupState.Error, BackupState.NotAvailable -> Unit BackupState.None -> { checkoutLauncher.launch(null) @@ -155,8 +149,12 @@ private fun BackupsSettingsContent( item { when (backupsSettingsState.backupState) { - BackupState.Loading -> { - LoadingBackupsRow() + is BackupState.LocalStore -> { + LocalStoreBackupRow( + backupState = backupsSettingsState.backupState, + lastBackupAt = backupsSettingsState.lastBackupAt, + onBackupsRowClick = onBackupsRowClick + ) OtherWaysToBackUpHeading() } @@ -419,6 +417,52 @@ private fun ViewSettingsButton(onClick: () -> Unit) { } } +@Composable +private fun LocalStoreBackupRow( + backupState: BackupState.LocalStore, + lastBackupAt: Duration, + onBackupsRowClick: () -> Unit +) { + Rows.TextRow( + modifier = Modifier.height(IntrinsicSize.Min), + icon = { + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .fillMaxHeight() + .padding(top = 12.dp) + ) { + Icon( + painter = painterResource(R.drawable.symbol_backup_24), + contentDescription = null + ) + } + }, + text = { + Column { + TextWithBetaLabel( + text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups), + textStyle = MaterialTheme.typography.bodyLarge + ) + + val tierText = when (backupState.tier) { + MessageBackupTier.FREE -> stringResource(R.string.RemoteBackupsSettingsFragment__your_backup_plan_is_free) + MessageBackupTier.PAID -> stringResource(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media) + } + + Text( + text = tierText, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) + + LastBackedUpText(lastBackupAt) + ViewSettingsButton(onBackupsRowClick) + } + } + ) +} + @Composable private fun ActiveBackupsRow( backupState: BackupState.WithTypeAndRenewalTime, @@ -483,24 +527,7 @@ private fun ActiveBackupsRow( } } - val lastBackupString = if (lastBackupAt.inWholeMilliseconds > 0) { - DateUtils.getDatelessRelativeTimeSpanFormattedDate( - LocalContext.current, - Locale.getDefault(), - lastBackupAt.inWholeMilliseconds - ).value - } else { - stringResource(R.string.RemoteBackupsSettingsFragment__never) - } - - Text( - text = stringResource( - R.string.BackupsSettingsFragment_last_backup_s, - lastBackupString - ), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium - ) + LastBackedUpText(lastBackupAt) ViewSettingsButton(onBackupsRowClick) } @@ -509,16 +536,25 @@ private fun ActiveBackupsRow( } @Composable -private fun LoadingBackupsRow() { - Box( - contentAlignment = Alignment.CenterStart, - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)) - ) { - CircularProgressIndicator() +private fun LastBackedUpText(lastBackupAt: Duration) { + val lastBackupString = if (lastBackupAt.inWholeMilliseconds > 0) { + DateUtils.getDatelessRelativeTimeSpanFormattedDate( + LocalContext.current, + Locale.getDefault(), + lastBackupAt.inWholeMilliseconds + ).value + } else { + stringResource(R.string.RemoteBackupsSettingsFragment__never) } + + Text( + text = stringResource( + R.string.BackupsSettingsFragment_last_backup_s, + lastBackupString + ), + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium + ) } @Composable @@ -664,14 +700,6 @@ private fun ActiveFreeBackupsRowPreview() { } } -@SignalPreview -@Composable -private fun LoadingBackupsRowPreview() { - Previews.Preview { - LoadingBackupsRow() - } -} - @SignalPreview @Composable private fun NeverEnabledBackupsRowPreview() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt index 1d16066eae..fb79fd6bd1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsState.kt @@ -14,7 +14,7 @@ import kotlin.time.Duration.Companion.milliseconds * Screen state for top-level backups settings screen. */ data class BackupsSettingsState( - val backupState: BackupState = BackupState.Loading, + val backupState: BackupState, val lastBackupAt: Duration = SignalStore.backup.lastBackupTime.milliseconds, val showBackupTierInternalOverride: Boolean = false, val backupTierInternalOverride: MessageBackupTier? = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt index 268da57ce4..c7c9869468 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupsSettingsViewModel.kt @@ -5,31 +5,21 @@ package org.thoughtcrime.securesms.components.settings.app.backups -import androidx.annotation.WorkerThread import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.asFlow import org.signal.core.util.concurrent.SignalDispatchers import org.signal.core.util.logging.Log -import org.signal.donations.InAppPaymentType import org.thoughtcrime.securesms.backup.DeletionState import org.thoughtcrime.securesms.backup.v2.MessageBackupTier -import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.Environment -import org.thoughtcrime.securesms.util.InternetConnectionObserver import org.thoughtcrime.securesms.util.RemoteConfig import kotlin.time.Duration.Companion.milliseconds @@ -39,61 +29,16 @@ class BackupsSettingsViewModel : ViewModel() { private val TAG = Log.tag(BackupsSettingsViewModel::class) } - private val internalStateFlow = MutableStateFlow(BackupsSettingsState()) + private val internalStateFlow: MutableStateFlow - val stateFlow: StateFlow = internalStateFlow - - private val loadRequests = MutableSharedFlow(extraBufferCapacity = 1) + val stateFlow: StateFlow by lazy { internalStateFlow } init { - viewModelScope.launch(SignalDispatchers.Default) { - InternetConnectionObserver.observe().asFlow() - .distinctUntilChanged() - .filter { it } - .drop(1) - .collect { - Log.d(TAG, "Triggering refresh from internet reconnect.") - loadRequests.emit(Unit) - } - } - - viewModelScope.launch(SignalDispatchers.Default) { - loadRequests.collect { - Log.d(TAG, "-- Dispatching state load.") - loadEnabledState().join() - Log.d(TAG, "-- Completed state load.") - } - } - - viewModelScope.launch(SignalDispatchers.Default) { - InAppPaymentsRepository.observeLatestBackupPayment().collect { - Log.d(TAG, "Triggering refresh from payment state change.") - loadRequests.emit(Unit) - } - } - } - - override fun onCleared() { - Log.d(TAG, "ViewModel has been cleared.") - } - - fun refreshState() { - Log.d(TAG, "Refreshing state from manual call.") - viewModelScope.launch(SignalDispatchers.Default) { - loadRequests.emit(Unit) - } - } - - @WorkerThread - private fun loadEnabledState(): Job { - return viewModelScope.launch(SignalDispatchers.IO) { - if (!RemoteConfig.messageBackups) { - Log.w(TAG, "Remote backups are not available on this device.") - internalStateFlow.update { it.copy(backupState = BackupState.NotAvailable, showBackupTierInternalOverride = false) } - } else { - val latestPurchase = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) - val enabledState = BackupStateRepository.resolveBackupState(latestPurchase) + val repo = BackupStateObserver(viewModelScope, useDatabaseFallbackOnNetworkError = true) + internalStateFlow = MutableStateFlow(BackupsSettingsState(backupState = repo.backupState.value)) + viewModelScope.launch { + repo.backupState.collect { enabledState -> Log.d(TAG, "Found enabled state $enabledState. Updating UI state.") internalStateFlow.update { it.copy( @@ -114,6 +59,7 @@ class BackupsSettingsViewModel : ViewModel() { SignalDatabase.recipients.markNeedsSync(Recipient.self().id) StorageSyncHelper.scheduleSyncForDataChange() } - refreshState() + + BackupStateObserver.notifyBackupTierChanged(scope = viewModelScope) } } 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 2074c4cc8a..58ccacc75d 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 @@ -468,7 +468,7 @@ private fun RemoteBackupsSettingsContent( item { when (state.backupState) { - is BackupState.Loading -> { + is BackupState.LocalStore -> { LoadingCard() } 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 87da0c9b69..049cfb578d 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 @@ -22,7 +22,7 @@ data class RemoteBackupsSettingsState( val hasRedemptionError: Boolean = false, val isOutOfStorageSpace: Boolean = false, val totalAllowedStorageSpace: String = "", - val backupState: BackupState = BackupState.Loading, + val backupState: BackupState, val backupMediaSize: Long = 0, val backupsFrequency: BackupFrequency = BackupFrequency.DAILY, val lastBackupTimestamp: Long = 0, 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 74d3777ff2..bd49424c14 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 @@ -35,7 +35,7 @@ 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.banner.banners.MediaRestoreProgressBanner -import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateRepository +import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase @@ -65,6 +65,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { private val _state = MutableStateFlow( RemoteBackupsSettingsState( tier = SignalStore.backup.backupTier, + backupState = BackupStateObserver.getNonIOBackupState(), backupsEnabled = SignalStore.backup.areBackupsEnabled, canBackupMessagesJobRun = BackupMessagesConstraint.isMet(AppDependencies.application), canViewBackupKey = !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application), @@ -155,6 +156,14 @@ class RemoteBackupsSettingsViewModel : ViewModel() { previous = current.state } } + + viewModelScope.launch { + BackupStateObserver(viewModelScope).backupState.collect { state -> + _state.update { + it.copy(backupState = state) + } + } + } } fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) { @@ -296,11 +305,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() { hasRedemptionError = lastPurchase?.data?.error?.data_ == "409" ) } - - val state = BackupStateRepository.resolveBackupState(lastPurchase) - _state.update { - it.copy(backupState = state) - } } private fun getBackupMediaSize(): Long { 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 58e0615180..63d642c65b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.backup.RestoreState 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.components.settings.app.backups.BackupStateObserver import org.thoughtcrime.securesms.jobmanager.impl.BackupMessagesConstraintObserver import org.thoughtcrime.securesms.jobmanager.impl.DeletionNotAwaitingMediaDownloadConstraint import org.thoughtcrime.securesms.jobmanager.impl.NoRemoteArchiveGarbageCollectionPendingConstraint @@ -212,12 +213,6 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { return MessageBackupTier.deserialize(getLong(KEY_LATEST_BACKUP_TIER, -1)) } - /** - * Denotes if there was a mismatch detected between the user's Signal subscription, on-device Google Play subscription, - * and what zk authorization we think we have. - */ - var subscriptionStateMismatchDetected: Boolean by booleanValue(KEY_SUBSCRIPTION_STATE_MISMATCH, false).withPrecondition { backupTierInternalOverride == null } - /** * When setting 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 @@ -247,11 +242,21 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { } else { putLong(KEY_BACKUP_TIER, serializedValue) } + + BackupStateObserver.notifyBackupTierChanged() } /** An internal setting that can override the backup tier for a user. */ var backupTierInternalOverride: MessageBackupTier? by enumValue(KEY_BACKUP_TIER_INTERNAL_OVERRIDE, null, MessageBackupTier.Serializer).withPrecondition { RemoteConfig.internalUser } + /** + * Denotes if there was a mismatch detected between the user's Signal subscription, on-device Google Play subscription, + * and what zk authorization we think we have. + */ + val subscriptionStateMismatchDetectedValue = booleanValue(KEY_SUBSCRIPTION_STATE_MISMATCH, false).withPrecondition { backupTierInternalOverride == null } + var subscriptionStateMismatchDetected: Boolean by subscriptionStateMismatchDetectedValue + val subscriptionStateMismatchDetectedFlow: Flow by lazy { subscriptionStateMismatchDetectedValue.toFlow() } + /** Set to true if we successfully restored a backup file timestamp or didn't find a file at all so a "no timestamp" value is restored. */ var isBackupTimestampRestored: Boolean by booleanValue(KEY_BACKUP_TIMESTAMP_RESTORED, false)