Fix backup state observation and expand logging.

This commit is contained in:
Alex Hart
2025-09-08 14:29:46 -03:00
committed by GitHub
parent 22382bc8a3
commit 5e35c209c2

View File

@@ -95,19 +95,19 @@ class BackupStateObserver(
} }
private val internalBackupState = MutableStateFlow(getNonIOBackupState()) private val internalBackupState = MutableStateFlow(getNonIOBackupState())
private val backupStateRefreshRequest = MutableSharedFlow<Unit>() private val backupStateRefreshRequest = MutableSharedFlow<Unit>(replay = 1)
val backupState: StateFlow<BackupState> = internalBackupState val backupState: StateFlow<BackupState> = internalBackupState
init { init {
scope.launch(SignalDispatchers.IO) { scope.launch(SignalDispatchers.IO) {
performDatabaseBackupStateRefresh() performDatabaseBackupStateRefresh()
}
scope.launch(SignalDispatchers.IO) { requestBackupStateRefresh()
backupStateRefreshRequest backupStateRefreshRequest
.throttleLatest(100.milliseconds) .throttleLatest(100.milliseconds)
.collect { .collect {
Log.d(TAG, "Dispatching refresh")
performFullBackupStateRefresh() performFullBackupStateRefresh()
} }
} }
@@ -160,19 +160,20 @@ class BackupStateObserver(
@WorkerThread @WorkerThread
private fun getDatabaseBackupState(): BackupState { private fun getDatabaseBackupState(): BackupState {
if (SignalStore.backup.backupTier != MessageBackupTier.PAID) { if (SignalStore.backup.backupTier != MessageBackupTier.PAID) {
Log.d(TAG, "No additional information available without accessing the network.") Log.d(TAG, "[getDatabaseBackupState] No additional information for non PAID backup available without accessing the network.")
return getNonIOBackupState() return getNonIOBackupState()
} }
val latestPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) val latestPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
if (latestPayment == null) { if (latestPayment == null) {
Log.d(TAG, "No additional information is available in the local database.") Log.d(TAG, "[getDatabaseBackupState] No additional information for PAID backup is available in the local database.")
return getNonIOBackupState() return getNonIOBackupState()
} }
val price = latestPayment.data.amount!!.toFiatMoney() val price = latestPayment.data.amount!!.toFiatMoney()
val isPending = SignalDatabase.inAppPayments.hasPendingBackupRedemption() val isPending = SignalDatabase.inAppPayments.hasPendingBackupRedemption()
if (isPending) { if (isPending) {
Log.d(TAG, "[getDatabaseBackupState] We have a pending subscription.")
return BackupState.Pending(price = price) return BackupState.Pending(price = price)
} }
@@ -184,6 +185,7 @@ class BackupStateObserver(
val isCanceled = latestPayment.data.cancellation != null val isCanceled = latestPayment.data.cancellation != null
if (isCanceled) { if (isCanceled) {
Log.d(TAG, "[getDatabaseBackupState] We have a canceled subscription.")
return BackupState.Canceled( return BackupState.Canceled(
messageBackupsType = paidBackupType, messageBackupsType = paidBackupType,
renewalTime = latestPayment.endOfPeriod renewalTime = latestPayment.endOfPeriod
@@ -191,6 +193,7 @@ class BackupStateObserver(
} }
if (SignalStore.backup.subscriptionStateMismatchDetected) { if (SignalStore.backup.subscriptionStateMismatchDetected) {
Log.d(TAG, "[getDatabaseBackupState] We have a subscription state mismatch with Google Play.")
return BackupState.SubscriptionMismatchMissingGooglePlay( return BackupState.SubscriptionMismatchMissingGooglePlay(
messageBackupsType = paidBackupType, messageBackupsType = paidBackupType,
renewalTime = latestPayment.endOfPeriod renewalTime = latestPayment.endOfPeriod
@@ -198,12 +201,14 @@ class BackupStateObserver(
} }
if (latestPayment.endOfPeriod < System.currentTimeMillis().milliseconds) { if (latestPayment.endOfPeriod < System.currentTimeMillis().milliseconds) {
Log.d(TAG, "[getDatabaseBackupState] We have an inactive subscription.")
return BackupState.Inactive( return BackupState.Inactive(
messageBackupsType = paidBackupType, messageBackupsType = paidBackupType,
renewalTime = latestPayment.endOfPeriod renewalTime = latestPayment.endOfPeriod
) )
} }
Log.d(TAG, "[getDatabaseBackupState] We have an active subscription.")
return BackupState.ActivePaid( return BackupState.ActivePaid(
messageBackupsType = paidBackupType, messageBackupsType = paidBackupType,
price = price, price = price,
@@ -213,16 +218,17 @@ class BackupStateObserver(
private suspend fun performDatabaseBackupStateRefresh() { private suspend fun performDatabaseBackupStateRefresh() {
if (!RemoteConfig.messageBackups) { if (!RemoteConfig.messageBackups) {
Log.d(TAG, "[performDatabaseBackupStateRefresh] Dropping refresh for disabled feature.")
return return
} }
if (!SignalStore.account.isRegistered) { if (!SignalStore.account.isRegistered) {
Log.d(TAG, "Dropping refresh for unregistered user.") Log.d(TAG, "[performDatabaseBackupStateRefresh] Dropping refresh for unregistered user.")
return return
} }
if (backupState.value !is BackupState.LocalStore) { if (backupState.value !is BackupState.LocalStore) {
Log.d(TAG, "Dropping database refresh for non-local store state.") Log.d(TAG, "[performDatabaseBackupStateRefresh] Dropping database refresh for non-local store state.")
return return
} }
@@ -231,15 +237,16 @@ class BackupStateObserver(
private suspend fun performFullBackupStateRefresh() { private suspend fun performFullBackupStateRefresh() {
if (!RemoteConfig.messageBackups) { if (!RemoteConfig.messageBackups) {
Log.d(TAG, "[performFullBackupStateRefresh] Dropping refresh for disabled feature.")
return return
} }
if (!SignalStore.account.isRegistered) { if (!SignalStore.account.isRegistered) {
Log.d(TAG, "Dropping refresh for unregistered user.") Log.d(TAG, "[performFullBackupStateRefresh] Dropping refresh for unregistered user.")
return return
} }
Log.d(TAG, "Performing refresh.") Log.d(TAG, "[performFullBackupStateRefresh] Performing refresh.")
withContext(SignalDispatchers.IO) { withContext(SignalDispatchers.IO) {
val latestInAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) val latestInAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
internalBackupState.emit(getNetworkBackupState(latestInAppPayment)) internalBackupState.emit(getNetworkBackupState(latestInAppPayment))
@@ -251,28 +258,28 @@ class BackupStateObserver(
*/ */
private suspend fun getNetworkBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState { private suspend fun getNetworkBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState {
if (lastPurchase?.state == InAppPaymentTable.State.PENDING) { if (lastPurchase?.state == InAppPaymentTable.State.PENDING) {
Log.d(TAG, "We have a pending subscription.") Log.d(TAG, "[getNetworkBackupState] We have a pending subscription.")
return BackupState.Pending( return BackupState.Pending(
price = lastPurchase.data.amount!!.toFiatMoney() price = lastPurchase.data.amount!!.toFiatMoney()
) )
} }
if (SignalStore.backup.subscriptionStateMismatchDetected) { if (SignalStore.backup.subscriptionStateMismatchDetected) {
Log.d(TAG, "[subscriptionStateMismatchDetected] A mismatch was detected.") Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] A mismatch was detected.")
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) { val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
is BillingPurchaseResult.Success -> { is BillingPurchaseResult.Success -> {
Log.d(TAG, "[subscriptionStateMismatchDetected] Found a purchase: $purchaseResult") Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Found a purchase: $purchaseResult")
purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing
} }
else -> { else -> {
Log.d(TAG, "[subscriptionStateMismatchDetected] No purchase found in Google Play Billing: $purchaseResult") Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] No purchase found in Google Play Billing: $purchaseResult")
false false
} }
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID } || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription") Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription")
val activeSubscription = withContext(Dispatchers.IO) { val activeSubscription = withContext(Dispatchers.IO) {
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull() RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
@@ -280,17 +287,18 @@ class BackupStateObserver(
val hasActiveSignalSubscription = activeSubscription?.isActive == true val hasActiveSignalSubscription = activeSubscription?.isActive == true
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription") Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription")
when { when {
hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> { hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
val type = buildPaidTypeFromSubscription(activeSubscription.activeSubscription) val type = buildPaidTypeFromSubscription(activeSubscription.activeSubscription)
if (type == null) { if (type == null) {
Log.d(TAG, "[subscriptionMismatchDetected] failed to load backup configuration. Likely a network error.") Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] failed to load backup configuration. Likely a network error.")
return getStateOnError() return getStateOnError()
} }
Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] found a subscription mismatch and successfully loaded configuration.")
return BackupState.SubscriptionMismatchMissingGooglePlay( return BackupState.SubscriptionMismatchMissingGooglePlay(
messageBackupsType = type, messageBackupsType = type,
renewalTime = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds renewalTime = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds
@@ -298,17 +306,17 @@ class BackupStateObserver(
} }
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> { hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
Log.d(TAG, "Found active signal subscription and active google play subscription. Clearing mismatch.") Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Found active signal subscription and active google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
} }
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> { !hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
Log.d(TAG, "Found inactive signal subscription and inactive google play subscription. Clearing mismatch.") Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
} }
else -> { else -> {
Log.w(TAG, "Hit unexpected subscription mismatch state: signal:false, google:true") Log.w(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Hit unexpected subscription mismatch state: signal:false, google:true")
return BackupState.NotFound return BackupState.NotFound
} }
} }
@@ -324,7 +332,7 @@ class BackupStateObserver(
} }
null -> { null -> {
Log.d(TAG, "Updating UI state with NONE null tier.") Log.d(TAG, "[getNetworkBackupState] Updating UI state with NONE null tier.")
return BackupState.None return BackupState.None
} }
} }
@@ -335,14 +343,16 @@ class BackupStateObserver(
*/ */
private fun getStateOnError(): BackupState { private fun getStateOnError(): BackupState {
return if (useDatabaseFallbackOnNetworkError) { return if (useDatabaseFallbackOnNetworkError) {
Log.d(TAG, "[getStateOnError] Getting fallback state from database.")
getDatabaseBackupState() getDatabaseBackupState()
} else { } else {
Log.d(TAG, "[getStateOnError] Displaying error without database.")
BackupState.Error BackupState.Error
} }
} }
private suspend fun getPaidBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState { private suspend fun getPaidBackupState(lastPurchase: InAppPaymentTable.InAppPayment?): BackupState {
Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.") Log.d(TAG, "[getPaidBackupState] Attempting to retrieve subscription details for active PAID backup.")
val typeResult = withContext(Dispatchers.IO) { val typeResult = withContext(Dispatchers.IO) {
BackupRepository.getPaidType() BackupRepository.getPaidType()
@@ -350,52 +360,62 @@ class BackupStateObserver(
val type = if (typeResult is NetworkResult.Success) typeResult.result else null val type = if (typeResult is NetworkResult.Success) typeResult.result else null
Log.d(TAG, "Attempting to retrieve current subscription...") Log.d(TAG, "[getPaidBackupState] Attempting to retrieve current subscription...")
val activeSubscription = withContext(Dispatchers.IO) { val activeSubscription = withContext(Dispatchers.IO) {
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
} }
return if (activeSubscription.isSuccess) { return if (activeSubscription.isSuccess) {
Log.d(TAG, "Retrieved subscription details.") Log.d(TAG, "[getPaidBackupState] Retrieved subscription details.")
val subscription = activeSubscription.getOrThrow().activeSubscription val subscription = activeSubscription.getOrThrow().activeSubscription
if (subscription != null) { if (subscription != null) {
Log.d(TAG, "Subscription found. Updating UI state with subscription details. Status: ${subscription.status}") Log.d(TAG, "[getPaidBackupState] Subscription found. Updating UI state with subscription details. Status: ${subscription.status}")
val subscriberType = type ?: buildPaidTypeFromSubscription(subscription) val subscriberType = type ?: buildPaidTypeFromSubscription(subscription)
if (subscriberType == null) { if (subscriberType == null) {
Log.d(TAG, "Failed to create backup type. Possible network error.") Log.d(TAG, "[getPaidBackupState] Failed to create backup type. Possible network error.")
getStateOnError() getStateOnError()
} else { } else {
when { when {
subscription.isCanceled && subscription.isActive -> BackupState.Canceled( subscription.isCanceled && subscription.isActive -> {
Log.d(TAG, "[getPaidBackupState] Found a canceled subscription.")
BackupState.Canceled(
messageBackupsType = subscriberType, messageBackupsType = subscriberType,
renewalTime = subscription.endOfCurrentPeriod.seconds renewalTime = subscription.endOfCurrentPeriod.seconds
) )
}
subscription.isActive -> BackupState.ActivePaid( subscription.isActive -> {
Log.d(TAG, "[getPaidBackupState] Found an active subscription.")
BackupState.ActivePaid(
messageBackupsType = subscriberType, messageBackupsType = subscriberType,
price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)), price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)),
renewalTime = subscription.endOfCurrentPeriod.seconds renewalTime = subscription.endOfCurrentPeriod.seconds
) )
}
else -> BackupState.Inactive( else -> {
Log.d(TAG, "[getPaidBackupState] Found an inactive subscription.")
BackupState.Inactive(
messageBackupsType = subscriberType, messageBackupsType = subscriberType,
renewalTime = subscription.endOfCurrentPeriod.seconds renewalTime = subscription.endOfCurrentPeriod.seconds
) )
} }
} }
}
} else { } else {
Log.d(TAG, "ActiveSubscription had null subscription object.") Log.d(TAG, "[getPaidBackupState] ActiveSubscription had null subscription object.")
if (SignalStore.backup.areBackupsEnabled) { if (SignalStore.backup.areBackupsEnabled) {
BackupState.NotFound BackupState.NotFound
} else if (lastPurchase != null && lastPurchase.endOfPeriod > System.currentTimeMillis().milliseconds) { } else if (lastPurchase != null && lastPurchase.endOfPeriod > System.currentTimeMillis().milliseconds) {
val canceledType = type ?: buildPaidTypeFromInAppPayment(lastPurchase) val canceledType = type ?: buildPaidTypeFromInAppPayment(lastPurchase)
if (canceledType == null) { if (canceledType == null) {
Log.w(TAG, "Failed to load canceled type information. Possible network error.") Log.w(TAG, "[getPaidBackupState] Failed to load canceled type information. Possible network error.")
getStateOnError() getStateOnError()
} else { } else {
Log.d(TAG, "[getPaidBackupState] Found a canceled subscription via the last purchase object.")
BackupState.Canceled( BackupState.Canceled(
messageBackupsType = canceledType, messageBackupsType = canceledType,
renewalTime = lastPurchase.endOfPeriod renewalTime = lastPurchase.endOfPeriod
@@ -404,9 +424,10 @@ class BackupStateObserver(
} else { } else {
val inactiveType = type ?: buildPaidTypeWithoutPricing() val inactiveType = type ?: buildPaidTypeWithoutPricing()
if (inactiveType == null) { if (inactiveType == null) {
Log.w(TAG, "Failed to load inactive type information. Possible network error.") Log.w(TAG, "[getPaidBackupState] Failed to load inactive type information. Possible network error.")
getStateOnError() getStateOnError()
} else { } else {
Log.d(TAG, "[getPaidBackupState] Found an inactive subscription via the last purchase object.")
BackupState.Inactive( BackupState.Inactive(
messageBackupsType = inactiveType, messageBackupsType = inactiveType,
renewalTime = lastPurchase?.endOfPeriod ?: 0.seconds renewalTime = lastPurchase?.endOfPeriod ?: 0.seconds
@@ -415,28 +436,32 @@ class BackupStateObserver(
} }
} }
} else { } else {
Log.d(TAG, "Failed to load ActiveSubscription data. Updating UI state with error.") Log.d(TAG, "[getPaidBackupState] Failed to load ActiveSubscription data. Updating UI state with error.")
getStateOnError() getStateOnError()
} }
} }
private suspend fun getFreeBackupState(): BackupState { private suspend fun getFreeBackupState(): BackupState {
Log.d(TAG, "[getFreeBackupState] Attempting to retrieve details for active FREE backup.")
val type = withContext(Dispatchers.IO) { val type = withContext(Dispatchers.IO) {
BackupRepository.getFreeType() BackupRepository.getFreeType()
} }
if (type !is NetworkResult.Success) { if (type !is NetworkResult.Success) {
Log.w(TAG, "Failed to load FREE type.", type.getCause()) Log.w(TAG, "[getFreeBackupState] Failed to load FREE type.", type.getCause())
return getStateOnError() return getStateOnError()
} }
val backupState = if (SignalStore.backup.areBackupsEnabled) { val backupState = if (SignalStore.backup.areBackupsEnabled) {
Log.d(TAG, "[getFreeBackupState] Found an active free backup.")
BackupState.ActiveFree(type.result) BackupState.ActiveFree(type.result)
} else { } else {
Log.d(TAG, "[getFreeBackupState] Found an inactive free backup.")
BackupState.Inactive(type.result) BackupState.Inactive(type.result)
} }
Log.d(TAG, "Updating UI state with $backupState FREE tier.") Log.d(TAG, "[getFreeBackupState] Updating UI state with $backupState FREE tier.")
return backupState return backupState
} }
@@ -446,7 +471,14 @@ class BackupStateObserver(
* @return A paid type, or null if we were unable to get the backup level configuration. * @return A paid type, or null if we were unable to get the backup level configuration.
*/ */
private fun buildPaidTypeFromSubscription(subscription: ActiveSubscription.Subscription): MessageBackupsType.Paid? { private fun buildPaidTypeFromSubscription(subscription: ActiveSubscription.Subscription): MessageBackupsType.Paid? {
val config = BackupRepository.getBackupLevelConfiguration().successOrThrow() val configResult = BackupRepository.getBackupLevelConfiguration()
if (configResult.getCause() != null) {
Log.w(TAG, "[buildPaidTypeFromSubscription] failed to build paid type.", configResult.getCause())
return null
}
// This should never throw
val config = configResult.successOrThrow()
val price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)) val price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
return MessageBackupsType.Paid( return MessageBackupsType.Paid(
@@ -462,7 +494,14 @@ class BackupStateObserver(
* @return A paid type, or null if we were unable to get the backup level configuration. * @return A paid type, or null if we were unable to get the backup level configuration.
*/ */
private fun buildPaidTypeFromInAppPayment(inAppPayment: InAppPaymentTable.InAppPayment): MessageBackupsType.Paid? { private fun buildPaidTypeFromInAppPayment(inAppPayment: InAppPaymentTable.InAppPayment): MessageBackupsType.Paid? {
val config = BackupRepository.getBackupLevelConfiguration().successOrThrow() val configResult = BackupRepository.getBackupLevelConfiguration()
if (configResult.getCause() != null) {
Log.w(TAG, "[buildPaidTypeFromInAppPayment] failed to build paid type.", configResult.getCause())
return null
}
// This should never throw
val config = configResult.successOrThrow()
val price = inAppPayment.data.amount!!.toFiatMoney() val price = inAppPayment.data.amount!!.toFiatMoney()
return MessageBackupsType.Paid( return MessageBackupsType.Paid(
@@ -479,7 +518,14 @@ class BackupStateObserver(
* @return A paid type, or null if we were unable to get the backup level configuration. * @return A paid type, or null if we were unable to get the backup level configuration.
*/ */
private fun buildPaidTypeWithoutPricing(): MessageBackupsType? { private fun buildPaidTypeWithoutPricing(): MessageBackupsType? {
val config = BackupRepository.getBackupLevelConfiguration().successOrThrow() val configResult = BackupRepository.getBackupLevelConfiguration()
if (configResult.getCause() != null) {
Log.w(TAG, "[buildPaidTypeWithoutPricing] failed to build paid type.", configResult.getCause())
return null
}
// This should never throw
val config = configResult.successOrThrow()
return MessageBackupsType.Paid( return MessageBackupsType.Paid(
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault())), pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault())),