Implement io-free state update and fallback mechanism for backups state.

This commit is contained in:
Alex Hart
2025-07-31 14:16:56 -03:00
committed by GitHub
parent bdeb5aa96a
commit 87a694c87c
9 changed files with 341 additions and 132 deletions

View File

@@ -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

View File

@@ -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<Unit>()
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<Unit>()
val backupState: StateFlow<BackupState> = 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) {

View File

@@ -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() {

View File

@@ -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

View File

@@ -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<BackupsSettingsState>
val stateFlow: StateFlow<BackupsSettingsState> = internalStateFlow
private val loadRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
val stateFlow: StateFlow<BackupsSettingsState> 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)
}
}

View File

@@ -468,7 +468,7 @@ private fun RemoteBackupsSettingsContent(
item {
when (state.backupState) {
is BackupState.Loading -> {
is BackupState.LocalStore -> {
LoadingCard()
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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<Boolean> 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)