diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/BackupUpgradeAvailabilityChecker.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/BackupUpgradeAvailabilityChecker.kt new file mode 100644 index 0000000000..35dc269048 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/BackupUpgradeAvailabilityChecker.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.ui.subscription + +import android.content.Context +import com.google.android.gms.common.GoogleApiAvailability +import org.thoughtcrime.securesms.dependencies.AppDependencies + +/** + * Delegate object for checking whether backup upgrade prompts should be shown to the user. + */ +object BackupUpgradeAvailabilityChecker { + + /** + * Best effort check for upgrade access. We check availability and show proper dialogs in the checkout + * flow, so this is fine as "best effort" + */ + suspend fun isUpgradeAvailable( + context: Context + ): Boolean { + val googlePlayServicesCode = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) + val googlePlayServicesAvailability = GooglePlayServicesAvailability.fromCode(googlePlayServicesCode) + + return when (googlePlayServicesAvailability) { + GooglePlayServicesAvailability.UNKNOWN, GooglePlayServicesAvailability.SERVICE_MISSING, GooglePlayServicesAvailability.SERVICE_DISABLED, GooglePlayServicesAvailability.SERVICE_INVALID -> false + GooglePlayServicesAvailability.SERVICE_VERSION_UPDATE_REQUIRED, GooglePlayServicesAvailability.SERVICE_UPDATING -> true + GooglePlayServicesAvailability.SUCCESS -> { + val billingResponseCode = AppDependencies.billingApi.getApiAvailability() + billingResponseCode.isSuccess + } + } + } +} 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 fcae09d3c6..0abf5c664b 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 @@ -507,6 +507,7 @@ private fun RemoteBackupsSettingsContent( backupState = state.backupState, onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick, isPaidTierPricingAvailable = state.isPaidTierPricingAvailable, + isGooglePlayServicesAvailable = state.isGooglePlayServicesAvailable, buttonsEnabled = backupDeleteState.isIdle() ) } @@ -994,6 +995,7 @@ private fun LazyListScope.appendBackupDetailsItems( private fun BackupCard( backupState: BackupState.WithTypeAndRenewalTime, isPaidTierPricingAvailable: Boolean, + isGooglePlayServicesAvailable: Boolean, buttonsEnabled: Boolean, onBackupTypeActionButtonClicked: (MessageBackupTier) -> Unit = {} ) { @@ -1085,7 +1087,7 @@ private fun BackupCard( ) } - if (backupState.isActive() && isPaidTierPricingAvailable) { + if (backupState.isActive() && isPaidTierPricingAvailable && isGooglePlayServicesAvailable) { val buttonText = when (messageBackupsType) { is MessageBackupsType.Paid -> stringResource(R.string.RemoteBackupsSettingsFragment__manage_or_cancel) is MessageBackupsType.Free -> stringResource(R.string.RemoteBackupsSettingsFragment__upgrade) @@ -1857,6 +1859,7 @@ private fun BackupCardPreview() { price = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")) ), isPaidTierPricingAvailable = true, + isGooglePlayServicesAvailable = true, buttonsEnabled = true ) } @@ -1872,6 +1875,7 @@ private fun BackupCardPreview() { renewalTime = 1727193018.seconds ), isPaidTierPricingAvailable = true, + isGooglePlayServicesAvailable = true, buttonsEnabled = true ) } @@ -1887,6 +1891,7 @@ private fun BackupCardPreview() { renewalTime = 1727193018.seconds ), isPaidTierPricingAvailable = true, + isGooglePlayServicesAvailable = true, buttonsEnabled = true ) } @@ -1903,6 +1908,7 @@ private fun BackupCardPreview() { price = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")) ), isPaidTierPricingAvailable = true, + isGooglePlayServicesAvailable = true, buttonsEnabled = true ) } @@ -1915,6 +1921,7 @@ private fun BackupCardPreview() { ) ), isPaidTierPricingAvailable = true, + isGooglePlayServicesAvailable = true, buttonsEnabled = true ) } @@ -1927,6 +1934,7 @@ private fun BackupCardPreview() { ) ), isPaidTierPricingAvailable = false, + isGooglePlayServicesAvailable = true, buttonsEnabled = true ) } 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 43da7bc18a..e7552ab8fa 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 @@ -32,7 +32,8 @@ data class RemoteBackupsSettingsState( val backupMediaDetails: BackupMediaDetails? = null, val showBackupCreateFailedError: Boolean = false, val showBackupCreateCouldNotCompleteError: Boolean = false, - val freeTierMediaRetentionDays: Int = -1 + val freeTierMediaRetentionDays: Int = -1, + val isGooglePlayServicesAvailable: Boolean = false ) { data class BackupMediaDetails( 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 4c57db8ced..ad71ea5067 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 @@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState.RestoreStatus import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.backup.v2.ui.subscription.BackupUpgradeAvailabilityChecker import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import org.thoughtcrime.securesms.components.settings.app.backups.BackupState import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver @@ -101,6 +102,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() { } } + viewModelScope.launch { + _state.update { + it.copy(isGooglePlayServicesAvailable = BackupUpgradeAvailabilityChecker.isUpgradeAvailable(AppDependencies.application)) + } + } + viewModelScope.launch(Dispatchers.IO) { refreshBackupMediaSizeState() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt index 22f90dcbf4..f041147fa6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/storage/ManageStorageSettingsViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.signal.core.util.concurrent.SignalExecutors import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +import org.thoughtcrime.securesms.backup.v2.ui.subscription.BackupUpgradeAvailabilityChecker import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.MediaTable @@ -133,7 +134,7 @@ class ManageStorageSettingsViewModel : ViewModel() { private suspend fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState { return when { - !SignalStore.backup.areBackupsEnabled || !AppDependencies.billingApi.getApiAvailability().isSuccess || (!RemoteConfig.internalUser && !Environment.IS_STAGING) -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE + !SignalStore.backup.areBackupsEnabled || !BackupUpgradeAvailabilityChecker.isUpgradeAvailable(AppDependencies.application) || (!RemoteConfig.internalUser && !Environment.IS_STAGING) -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE SignalStore.backup.backupTier != MessageBackupTier.PAID -> OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED else -> OnDeviceStorageOptimizationState.DISABLED diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 2fc4f8ed59..1572f6eed4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -118,6 +118,7 @@ import org.thoughtcrime.securesms.MuteDialog import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.attachments.AttachmentSaver import org.thoughtcrime.securesms.audio.AudioRecorder +import org.thoughtcrime.securesms.backup.v2.ui.subscription.BackupUpgradeAvailabilityChecker import org.thoughtcrime.securesms.badges.gifts.OpenableGift import org.thoughtcrime.securesms.badges.gifts.OpenableGiftItemDecoration import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet @@ -3069,10 +3070,14 @@ class ConversationFragment : } override fun onDisplayMediaNoLongerAvailableSheet() { - if (SignalStore.backup.areBackupsEnabled) { - UpgradeToStartMediaBackupSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) - } else { - MediaNoLongerAvailableBottomSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + viewLifecycleOwner.lifecycleScope.launch { + val isUpgradeAvailable = BackupUpgradeAvailabilityChecker.isUpgradeAvailable(requireContext()) + + if (SignalStore.backup.areBackupsEnabled && isUpgradeAvailable) { + UpgradeToStartMediaBackupSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } else { + MediaNoLongerAvailableBottomSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } } }