Add "Ready to download" state and clear out a few TODOs.

This commit is contained in:
Alex Hart 2024-10-31 09:54:58 -03:00 committed by GitHub
parent 21c359f919
commit 7e93e15a9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 234 additions and 23 deletions

View file

@ -20,6 +20,8 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color

View file

@ -126,12 +126,16 @@ private fun BackupsSettingsContent(
when (backupsSettingsState.enabledState) { when (backupsSettingsState.enabledState) {
BackupsSettingsState.EnabledState.Loading -> { BackupsSettingsState.EnabledState.Loading -> {
LoadingBackupsRow() LoadingBackupsRow()
OtherWaysToBackUpHeading()
} }
BackupsSettingsState.EnabledState.Inactive -> { BackupsSettingsState.EnabledState.Inactive -> {
InactiveBackupsRow( InactiveBackupsRow(
onBackupsRowClick = onBackupsRowClick onBackupsRowClick = onBackupsRowClick
) )
OtherWaysToBackUpHeading()
} }
is BackupsSettingsState.EnabledState.Active -> { is BackupsSettingsState.EnabledState.Active -> {
@ -139,29 +143,28 @@ private fun BackupsSettingsContent(
enabledState = backupsSettingsState.enabledState, enabledState = backupsSettingsState.enabledState,
onBackupsRowClick = onBackupsRowClick onBackupsRowClick = onBackupsRowClick
) )
OtherWaysToBackUpHeading()
} }
BackupsSettingsState.EnabledState.Never -> { BackupsSettingsState.EnabledState.Never -> {
NeverEnabledBackupsRow( NeverEnabledBackupsRow(
onBackupsRowClick = onBackupsRowClick onBackupsRowClick = onBackupsRowClick
) )
OtherWaysToBackUpHeading()
} }
BackupsSettingsState.EnabledState.Failed -> { BackupsSettingsState.EnabledState.Failed -> {
Text(text = "TODO") WaitingForNetworkRow()
OtherWaysToBackUpHeading()
} }
BackupsSettingsState.EnabledState.NotAvailable -> Unit
} }
} }
item { item {
Dividers.Default()
}
item {
Texts.SectionHeader(
text = stringResource(R.string.RemoteBackupsSettingsFragment__other_ways_to_backup)
)
Rows.TextRow( Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups), text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups),
label = stringResource(R.string.RemoteBackupsSettingsFragment__save_your_backups_to), label = stringResource(R.string.RemoteBackupsSettingsFragment__save_your_backups_to),
@ -172,6 +175,15 @@ private fun BackupsSettingsContent(
} }
} }
@Composable
private fun OtherWaysToBackUpHeading() {
Dividers.Default()
Texts.SectionHeader(
text = stringResource(R.string.RemoteBackupsSettingsFragment__other_ways_to_backup)
)
}
@Composable @Composable
private fun NeverEnabledBackupsRow( private fun NeverEnabledBackupsRow(
onBackupsRowClick: () -> Unit = {} onBackupsRowClick: () -> Unit = {}
@ -215,6 +227,18 @@ private fun NeverEnabledBackupsRow(
) )
} }
@Composable
private fun WaitingForNetworkRow() {
Rows.TextRow(
text = {
Text(text = stringResource(R.string.RemoteBackupsSettingsFragment__waiting_for_network))
},
icon = {
CircularProgressIndicator()
}
)
}
@Composable @Composable
private fun InactiveBackupsRow( private fun InactiveBackupsRow(
onBackupsRowClick: () -> Unit = {} onBackupsRowClick: () -> Unit = {}
@ -327,6 +351,26 @@ private fun BackupsSettingsContentPreview() {
} }
} }
@SignalPreview
@Composable
private fun BackupsSettingsContentNotAvailablePreview() {
Previews.Preview {
BackupsSettingsContent(
backupsSettingsState = BackupsSettingsState(
enabledState = BackupsSettingsState.EnabledState.NotAvailable
)
)
}
}
@SignalPreview
@Composable
private fun WaitingForNetworkRowPreview() {
Previews.Preview {
WaitingForNetworkRow()
}
}
@SignalPreview @SignalPreview
@Composable @Composable
private fun InactiveBackupsRowPreview() { private fun InactiveBackupsRowPreview() {

View file

@ -23,6 +23,11 @@ data class BackupsSettingsState(
*/ */
data object Loading : EnabledState data object Loading : EnabledState
/**
* Google Play Billing is not available on this device
*/
data object NotAvailable : EnabledState
/** /**
* Backups have never been enabled. * Backups have never been enabled.
*/ */

View file

@ -23,8 +23,10 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.InternetConnectionObserver import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.Currency import java.util.Currency
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -58,6 +60,11 @@ class BackupsSettingsViewModel : ViewModel() {
private fun loadEnabledState() { private fun loadEnabledState() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
if (!RemoteConfig.messageBackups || !AppDependencies.billingApi.isApiAvailable()) {
internalStateFlow.update { it.copy(enabledState = BackupsSettingsState.EnabledState.NotAvailable) }
return@launch
}
val enabledState = when (SignalStore.backup.backupTier) { val enabledState = when (SignalStore.backup.backupTier) {
MessageBackupTier.FREE -> getEnabledStateForFreeTier() MessageBackupTier.FREE -> getEnabledStateForFreeTier()
MessageBackupTier.PAID -> getEnabledStateForPaidTier() MessageBackupTier.PAID -> getEnabledStateForPaidTier()

View file

@ -10,7 +10,12 @@ import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
/** /**
* State container for BackupStatusData, including the enabled state. * State container for BackupStatusData, including the enabled state.
*/ */
data class BackupRestoreState( sealed interface BackupRestoreState {
val enabled: Boolean, data object None : BackupRestoreState
val backupStatusData: BackupStatusData data class Ready(
) val bytes: String
) : BackupRestoreState
data class FromBackupStatusData(
val backupStatusData: BackupStatusData
) : BackupRestoreState
}

View file

@ -58,7 +58,9 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -99,6 +101,7 @@ import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Currency import java.util.Currency
import java.util.Locale import java.util.Locale
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@ -199,12 +202,20 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
} }
} }
override fun onStartMediaRestore() {
// TODO - [backups] Begin media restore.
}
override fun onCancelMediaRestore() { override fun onCancelMediaRestore() {
// TODO - [backups] Cancel media restoration // TODO - [backups] Cancel in-progress media restoration
}
override fun onDisplaySkipMediaRestoreProtectionDialog() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.SKIP_MEDIA_RESTORE_PROTECTION)
} }
override fun onSkipMediaRestore() { override fun onSkipMediaRestore() {
// TODO - [backups] Skip media restoration // TODO - [backups] Skip disk-full media restoration
} }
override fun onLearnMoreAboutLostSubscription() { override fun onLearnMoreAboutLostSubscription() {
@ -292,6 +303,8 @@ private interface ContentCallbacks {
fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) = Unit fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) = Unit
fun onTurnOffAndDeleteBackupsConfirm() = Unit fun onTurnOffAndDeleteBackupsConfirm() = Unit
fun onViewBackupKeyClick() = Unit fun onViewBackupKeyClick() = Unit
fun onStartMediaRestore() = Unit
fun onDisplaySkipMediaRestoreProtectionDialog() = Unit
fun onSkipMediaRestore() = Unit fun onSkipMediaRestore() = Unit
fun onCancelMediaRestore() = Unit fun onCancelMediaRestore() = Unit
fun onRenewLostSubscription() = Unit fun onRenewLostSubscription() = Unit
@ -365,6 +378,30 @@ private fun RemoteBackupsSettingsContent(
} }
if (backupsEnabled) { if (backupsEnabled) {
if (backupRestoreState !is BackupRestoreState.None) {
item {
Dividers.Default()
}
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
item {
BackupStatusRow(
backupStatusData = backupRestoreState.backupStatusData,
onCancelClick = contentCallbacks::onCancelMediaRestore,
onSkipClick = contentCallbacks::onSkipMediaRestore
)
}
} else if (backupRestoreState is BackupRestoreState.Ready && backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
item {
BackupReadyToDownloadRow(
ready = backupRestoreState,
endOfSubscription = backupState.renewalTime,
onDownloadClick = contentCallbacks::onStartMediaRestore
)
}
}
}
appendBackupDetailsItems( appendBackupDetailsItems(
backupProgress = backupProgress, backupProgress = backupProgress,
lastBackupTimestamp = lastBackupTimestamp, lastBackupTimestamp = lastBackupTimestamp,
@ -374,7 +411,7 @@ private fun RemoteBackupsSettingsContent(
contentCallbacks = contentCallbacks contentCallbacks = contentCallbacks
) )
} else { } else {
if (backupRestoreState.enabled) { if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
item { item {
BackupStatusRow( BackupStatusRow(
backupStatusData = backupRestoreState.backupStatusData, backupStatusData = backupRestoreState.backupStatusData,
@ -443,6 +480,18 @@ private fun RemoteBackupsSettingsContent(
onContactSupport = contentCallbacks::onContactSupport onContactSupport = contentCallbacks::onContactSupport
) )
} }
RemoteBackupsSettingsState.Dialog.SKIP_MEDIA_RESTORE_PROTECTION -> {
SkipDownloadDialog(
renewalTime = if (backupState is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime) {
backupState.renewalTime
} else {
error("Unexpected dialog display without renewal time.")
},
onDismiss = contentCallbacks::onDialogDismissed,
onSkipClick = contentCallbacks::onSkipMediaRestore
)
}
} }
val snackbarMessageId = remember(requestedSnackbar) { val snackbarMessageId = remember(requestedSnackbar) {
@ -931,8 +980,8 @@ private fun FailedToTurnOffBackupDialog(
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
Dialogs.SimpleAlertDialog( Dialogs.SimpleAlertDialog(
title = "TODO", title = stringResource(R.string.RemoteBackupsSettingsFragment__couldnt_turn_off_and_delete_backups),
body = "TODO", body = stringResource(R.string.RemoteBackupsSettingsFragment__a_network_error_occurred),
confirm = stringResource(id = android.R.string.ok), confirm = stringResource(id = android.R.string.ok),
onConfirm = {}, onConfirm = {},
onDismiss = onDismiss onDismiss = onDismiss
@ -968,6 +1017,25 @@ private fun DownloadingYourBackupDialog(
) )
} }
@Composable
private fun SkipDownloadDialog(
renewalTime: Duration,
onSkipClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
val days = (renewalTime - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.RemoteBackupsSettingsFragment__skip_download_question),
body = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__if_you_skip_downloading, days, days),
confirm = stringResource(R.string.RemoteBackupsSettingsFragment__skip),
dismiss = stringResource(android.R.string.cancel),
confirmColor = MaterialTheme.colorScheme.error,
onConfirm = onSkipClick,
onDismiss = onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun CircularProgressDialog( private fun CircularProgressDialog(
@ -1055,6 +1123,38 @@ private fun BackupFrequencyDialog(
} }
} }
@Composable
private fun BackupReadyToDownloadRow(
ready: BackupRestoreState.Ready,
endOfSubscription: Duration,
onDownloadClick: () -> Unit = {}
) {
val days = (endOfSubscription - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
val string = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
val annotated = buildAnnotatedString {
append(string)
val startIndex = string.indexOf(ready.bytes)
val endIndex = startIndex + ready.bytes.length
addStyle(SpanStyle(fontWeight = FontWeight.Bold), startIndex, endIndex)
}
Column {
Text(
text = annotated,
modifier = Modifier
.horizontalGutters()
.padding(vertical = 8.dp)
)
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__download),
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
onClick = onDownloadClick
)
}
}
@Composable @Composable
private fun getTextForFrequency(backupsFrequency: BackupFrequency): String { private fun getTextForFrequency(backupsFrequency: BackupFrequency): String {
return when (backupsFrequency) { return when (backupsFrequency) {
@ -1082,7 +1182,7 @@ private fun RemoteBackupsSettingsContentPreview() {
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree( backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30) messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30)
), ),
backupRestoreState = BackupRestoreState(false, BackupStatusData.CouldNotCompleteBackup) backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup)
) )
} }
} }
@ -1187,6 +1287,17 @@ private fun BackupCardPreview() {
} }
} }
@SignalPreview
@Composable
private fun BackupReadyToDownloadPreview() {
Previews.Preview {
BackupReadyToDownloadRow(
ready = BackupRestoreState.Ready("12GB"),
endOfSubscription = System.currentTimeMillis().milliseconds + 30.days
)
}
}
@SignalPreview @SignalPreview
@Composable @Composable
private fun LastBackupRowPreview() { private fun LastBackupRowPreview() {
@ -1237,6 +1348,16 @@ private fun DownloadingYourBackupDialogPreview() {
} }
} }
@SignalPreview
@Composable
private fun SkipDownloadDialogPreview() {
Previews.Preview {
SkipDownloadDialog(
renewalTime = System.currentTimeMillis().milliseconds + 30.days
)
}
}
@SignalPreview @SignalPreview
@Composable @Composable
private fun CircularProgressDialogPreview() { private fun CircularProgressDialogPreview() {

View file

@ -112,7 +112,8 @@ data class RemoteBackupsSettingsState(
PROGRESS_SPINNER, PROGRESS_SPINNER,
DOWNLOADING_YOUR_BACKUP, DOWNLOADING_YOUR_BACKUP,
TURN_OFF_FAILED, TURN_OFF_FAILED,
SUBSCRIPTION_NOT_FOUND SUBSCRIPTION_NOT_FOUND,
SKIP_MEDIA_RESTORE_PROTECTION
} }
enum class Snackbar { enum class Snackbar {

View file

@ -21,13 +21,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.signal.core.util.billing.BillingPurchaseResult import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.bytes
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.v2.BackupFrequency import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupRepository import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatMoney
@ -64,7 +64,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
) )
) )
private val _restoreState: MutableStateFlow<BackupRestoreState> = MutableStateFlow(BackupRestoreState(false, BackupStatusData.RestoringMedia())) private val _restoreState: MutableStateFlow<BackupRestoreState> = MutableStateFlow(BackupRestoreState.None)
private val latestPurchaseId = MutableSharedFlow<InAppPaymentTable.InAppPaymentId>() private val latestPurchaseId = MutableSharedFlow<InAppPaymentTable.InAppPaymentId>()
val state: StateFlow<RemoteBackupsSettingsState> = _state val state: StateFlow<RemoteBackupsSettingsState> = _state
@ -86,8 +86,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
if (restoreProgress.enabled) { if (restoreProgress.enabled) {
Log.d(TAG, "Backup is being restored. Collecting updates.") Log.d(TAG, "Backup is being restored. Collecting updates.")
restoreProgress.dataFlow.collectLatest { latest -> restoreProgress.dataFlow.collectLatest { latest ->
_restoreState.update { BackupRestoreState(restoreProgress.enabled, latest) } _restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
} }
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
} else {
_restoreState.update { BackupRestoreState.None }
} }
delay(1.seconds) delay(1.seconds)
@ -218,10 +222,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
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
) )
subscription.isCanceled -> RemoteBackupsSettingsState.BackupState.Canceled( subscription.isCanceled -> RemoteBackupsSettingsState.BackupState.Canceled(
messageBackupsType = type, messageBackupsType = type,
renewalTime = subscription.endOfCurrentPeriod.seconds renewalTime = subscription.endOfCurrentPeriod.seconds
) )
else -> RemoteBackupsSettingsState.BackupState.Inactive( else -> RemoteBackupsSettingsState.BackupState.Inactive(
messageBackupsType = type, messageBackupsType = type,
renewalTime = subscription.endOfCurrentPeriod.seconds renewalTime = subscription.endOfCurrentPeriod.seconds

View file

@ -7694,6 +7694,26 @@
<string name="RemoteBackupsSettingsFragment__renew">Renew</string> <string name="RemoteBackupsSettingsFragment__renew">Renew</string>
<!-- Button label to learn more about why subscription disappeared --> <!-- Button label to learn more about why subscription disappeared -->
<string name="RemoteBackupsSettingsFragment__learn_more">Learn more</string> <string name="RemoteBackupsSettingsFragment__learn_more">Learn more</string>
<!-- Displayed in row when backup is available for download and users subscription has expired. First placeholder is data size e.g. 12MB, second is days before expiration -->
<plurals name="RemoteBackupsSettingsFragment__you_have_s_of_backup_data">
<item quantity="one">You have %1$s of backup data thats not on this device. Your backup will be deleted when your subscription ends in %2$d day.</item>
<item quantity="other">You have %1$s of backup data thats not on this device. Your backup will be deleted when your subscription ends in %2$d days.</item>
</plurals>
<!-- Displayed in row when backup is available for download to let user start download -->
<string name="RemoteBackupsSettingsFragment__download">Download</string>
<!-- Dialog title for skipping download of backed up media -->
<string name="RemoteBackupsSettingsFragment__skip_download_question">Skip download?</string>
<!-- Dialog body for skiping download of backed up media -->
<plurals name="RemoteBackupsSettingsFragment__if_you_skip_downloading">
<item quantity="one">If you skip downloading the remaining media and attachments in your backup will be deleted in %1$d day.</item>
<item quantity="other">If you skip downloading the remaining media and attachments in your backup will be deleted in %1$d days.</item>
</plurals>
<!-- Positive dialog action to skip download -->
<string name="RemoteBackupsSettingsFragment__skip">Skip</string>
<!-- Dialog title for network error while trying to disable backups -->
<string name="RemoteBackupsSettingsFragment__couldnt_turn_off_and_delete_backups">Couldn\'t turn off and delete backups</string>
<!-- Dialog body for network error while trying to disable backups -->
<string name="RemoteBackupsSettingsFragment__a_network_error_occurred">A network error occurred. Please check your internet connection and try again.</string>
<!-- SubscriptionNotFoundBottomSheet --> <!-- SubscriptionNotFoundBottomSheet -->
<!-- Displayed as a bottom sheet title --> <!-- Displayed as a bottom sheet title -->