mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Add internal setting for forcing backup tier.
This commit is contained in:
committed by
Cody Henthorne
parent
ac4db23709
commit
3727a8e1df
@@ -132,6 +132,9 @@ object BackupRepository {
|
||||
}
|
||||
|
||||
403 -> {
|
||||
if (SignalStore.backup.backupTierInternalOverride != null) {
|
||||
Log.w(TAG, "Received status 403, but the internal override is set, so not doing anything.", error.exception)
|
||||
} else {
|
||||
Log.w(TAG, "Received status 403. The user is not in the media tier. Updating local state.", error.exception)
|
||||
SignalStore.backup.backupTier = MessageBackupTier.FREE
|
||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||
@@ -139,6 +142,7 @@ object BackupRepository {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers backup id reservation. As documented, this is safe to perform multiple times.
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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
|
||||
@@ -19,9 +20,11 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -95,7 +98,8 @@ class BackupsSettingsFragment : ComposeFragment() {
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) }
|
||||
onOnDeviceBackupsRowClick = { findNavController().safeNavigate(R.id.action_backupsSettingsFragment_to_backupsPreferenceFragment) },
|
||||
onBackupTierInternalOverrideChanged = { viewModel.onBackupTierInternalOverrideChanged(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -105,7 +109,8 @@ private fun BackupsSettingsContent(
|
||||
backupsSettingsState: BackupsSettingsState,
|
||||
onNavigationClick: () -> Unit = {},
|
||||
onBackupsRowClick: () -> Unit = {},
|
||||
onOnDeviceBackupsRowClick: () -> Unit = {}
|
||||
onOnDeviceBackupsRowClick: () -> Unit = {},
|
||||
onBackupTierInternalOverrideChanged: (MessageBackupTier?) -> Unit = {}
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = stringResource(R.string.preferences_chats__backups),
|
||||
@@ -115,6 +120,23 @@ private fun BackupsSettingsContent(
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
) {
|
||||
if (backupsSettingsState.showBackupTierInternalOverride) {
|
||||
item {
|
||||
Column(modifier = Modifier.padding(horizontal = dimensionResource(id = org.signal.core.ui.R.dimen.gutter))) {
|
||||
Text(
|
||||
text = "INTERNAL ONLY",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Text(
|
||||
text = "Use this to override the subscription state to one of your choosing.",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
InternalBackupOverrideRow(backupsSettingsState, onBackupTierInternalOverrideChanged)
|
||||
}
|
||||
Dividers.Default()
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Text(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__back_up_your_message_history),
|
||||
@@ -334,6 +356,30 @@ private fun LoadingBackupsRow() {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InternalBackupOverrideRow(
|
||||
backupsSettingsState: BackupsSettingsState,
|
||||
onBackupTierInternalOverrideChanged: (MessageBackupTier?) -> Unit = {}
|
||||
) {
|
||||
val options = remember {
|
||||
mapOf(
|
||||
"Unset" to null,
|
||||
"Free" to MessageBackupTier.FREE,
|
||||
"Paid" to MessageBackupTier.PAID
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
options.forEach { option ->
|
||||
RadioButton(
|
||||
selected = option.value == backupsSettingsState.backupTierInternalOverride,
|
||||
onClick = { onBackupTierInternalOverrideChanged(option.value) }
|
||||
)
|
||||
Text(option.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupsSettingsContentPreview() {
|
||||
@@ -366,6 +412,20 @@ private fun BackupsSettingsContentNotAvailablePreview() {
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun BackupsSettingsContentBackupTierInternalOverridePreview() {
|
||||
Previews.Preview {
|
||||
BackupsSettingsContent(
|
||||
backupsSettingsState = BackupsSettingsState(
|
||||
enabledState = BackupsSettingsState.EnabledState.Never,
|
||||
showBackupTierInternalOverride = true,
|
||||
backupTierInternalOverride = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun WaitingForNetworkRowPreview() {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.app.backups
|
||||
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import kotlin.time.Duration
|
||||
|
||||
@@ -12,7 +13,9 @@ import kotlin.time.Duration
|
||||
* Screen state for top-level backups settings screen.
|
||||
*/
|
||||
data class BackupsSettingsState(
|
||||
val enabledState: EnabledState = EnabledState.Loading
|
||||
val enabledState: EnabledState = EnabledState.Loading,
|
||||
val showBackupTierInternalOverride: Boolean = false,
|
||||
val backupTierInternalOverride: MessageBackupTier? = null
|
||||
) {
|
||||
/**
|
||||
* Describes the 'enabled' state of backups.
|
||||
|
||||
@@ -61,7 +61,7 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
private fun loadEnabledState() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (!RemoteConfig.messageBackups || !AppDependencies.billingApi.isApiAvailable()) {
|
||||
internalStateFlow.update { it.copy(enabledState = BackupsSettingsState.EnabledState.NotAvailable) }
|
||||
internalStateFlow.update { it.copy(enabledState = BackupsSettingsState.EnabledState.NotAvailable, showBackupTierInternalOverride = false) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
@@ -71,10 +71,15 @@ class BackupsSettingsViewModel : ViewModel() {
|
||||
null -> getEnabledStateForNoTier()
|
||||
}
|
||||
|
||||
internalStateFlow.update { it.copy(enabledState = enabledState) }
|
||||
internalStateFlow.update { it.copy(enabledState = enabledState, showBackupTierInternalOverride = RemoteConfig.internalUser, backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onBackupTierInternalOverrideChanged(tier: MessageBackupTier?) {
|
||||
SignalStore.backup.backupTierInternalOverride = tier
|
||||
refreshState()
|
||||
}
|
||||
|
||||
private suspend fun getEnabledStateForFreeTier(): BackupsSettingsState.EnabledState {
|
||||
return try {
|
||||
BackupsSettingsState.EnabledState.Active(
|
||||
|
||||
@@ -188,7 +188,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
|
||||
is BillingPurchaseResult.Success -> purchaseResult.isAcknowledged && purchaseResult.isWithinTheLastMonth()
|
||||
else -> false
|
||||
}
|
||||
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
|
||||
|
||||
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription")
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.donations.PaymentSourceType
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.badges.Badges
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType
|
||||
@@ -30,6 +31,8 @@ import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import org.whispersystems.signalservice.internal.EmptyResponse
|
||||
import org.whispersystems.signalservice.internal.ServiceResponse
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -52,11 +55,33 @@ object RecurringInAppPaymentRepository {
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
/** A fake paid subscription to return when the backup tier override is set. */
|
||||
private val MOCK_PAID_SUBSCRIPTION = ActiveSubscription(
|
||||
ActiveSubscription.Subscription(
|
||||
SubscriptionsConfiguration.BACKUPS_LEVEL,
|
||||
"USD",
|
||||
BigDecimal(42),
|
||||
2147472000,
|
||||
true,
|
||||
2147472000,
|
||||
false,
|
||||
"active",
|
||||
"USA",
|
||||
"credit-card",
|
||||
false
|
||||
),
|
||||
null
|
||||
)
|
||||
|
||||
/**
|
||||
* Gets the active subscription if it exists for the given [InAppPaymentSubscriberRecord.Type]
|
||||
*/
|
||||
@WorkerThread
|
||||
fun getActiveSubscriptionSync(type: InAppPaymentSubscriberRecord.Type): Result<ActiveSubscription> {
|
||||
if (SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID) {
|
||||
return Result.success(MOCK_PAID_SUBSCRIPTION)
|
||||
}
|
||||
|
||||
val response = InAppPaymentsRepository.getSubscriber(type)?.let {
|
||||
donationsService.getSubscription(it.subscriberId)
|
||||
} ?: return Result.success(ActiveSubscription.EMPTY)
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.backup.v2.BackupFrequency
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
|
||||
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
|
||||
@@ -35,6 +36,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_BACKUP_TIER_INTERNAL_OVERRIDE = "backup.backupTier.internalOverride"
|
||||
private const val KEY_BACKUP_TIER_RESTORED = "backup.backupTierRestored"
|
||||
private const val KEY_LATEST_BACKUP_TIER = "backup.latestBackupTier"
|
||||
private const val KEY_LAST_CHECK_IN_MILLIS = "backup.lastCheckInMilliseconds"
|
||||
@@ -164,13 +166,17 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
* 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)
|
||||
val latestBackupTier: MessageBackupTier?
|
||||
get() {
|
||||
backupTierInternalOverride?.let { return it }
|
||||
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)
|
||||
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
|
||||
@@ -178,7 +184,10 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
* use in the UI
|
||||
*/
|
||||
var backupTier: MessageBackupTier?
|
||||
get() = MessageBackupTier.deserialize(getLong(KEY_BACKUP_TIER, -1))
|
||||
get() {
|
||||
backupTierInternalOverride?.let { return it }
|
||||
return MessageBackupTier.deserialize(getLong(KEY_BACKUP_TIER, -1))
|
||||
}
|
||||
set(value) {
|
||||
Log.i(TAG, "Setting backup tier to $value", Throwable(), true)
|
||||
val serializedValue = MessageBackupTier.serialize(value)
|
||||
@@ -193,6 +202,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 }
|
||||
|
||||
var isBackupTierRestored: Boolean by booleanValue(KEY_BACKUP_TIER_RESTORED, false)
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user