Add "Ready to download" state and clear out a few TODOs.
This commit is contained in:
parent
21c359f919
commit
7e93e15a9b
9 changed files with 234 additions and 23 deletions
|
@ -20,6 +20,8 @@ import androidx.compose.material3.LinearProgressIndicator
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
|
@ -126,12 +126,16 @@ private fun BackupsSettingsContent(
|
|||
when (backupsSettingsState.enabledState) {
|
||||
BackupsSettingsState.EnabledState.Loading -> {
|
||||
LoadingBackupsRow()
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Inactive -> {
|
||||
InactiveBackupsRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
is BackupsSettingsState.EnabledState.Active -> {
|
||||
|
@ -139,29 +143,28 @@ private fun BackupsSettingsContent(
|
|||
enabledState = backupsSettingsState.enabledState,
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Never -> {
|
||||
NeverEnabledBackupsRow(
|
||||
onBackupsRowClick = onBackupsRowClick
|
||||
)
|
||||
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.Failed -> {
|
||||
Text(text = "TODO")
|
||||
WaitingForNetworkRow()
|
||||
OtherWaysToBackUpHeading()
|
||||
}
|
||||
|
||||
BackupsSettingsState.EnabledState.NotAvailable -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Dividers.Default()
|
||||
}
|
||||
|
||||
item {
|
||||
Texts.SectionHeader(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__other_ways_to_backup)
|
||||
)
|
||||
|
||||
Rows.TextRow(
|
||||
text = stringResource(R.string.RemoteBackupsSettingsFragment__on_device_backups),
|
||||
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
|
||||
private fun NeverEnabledBackupsRow(
|
||||
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
|
||||
private fun InactiveBackupsRow(
|
||||
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
|
||||
@Composable
|
||||
private fun InactiveBackupsRowPreview() {
|
||||
|
|
|
@ -23,6 +23,11 @@ data class BackupsSettingsState(
|
|||
*/
|
||||
data object Loading : EnabledState
|
||||
|
||||
/**
|
||||
* Google Play Billing is not available on this device
|
||||
*/
|
||||
data object NotAvailable : EnabledState
|
||||
|
||||
/**
|
||||
* Backups have never been enabled.
|
||||
*/
|
||||
|
|
|
@ -23,8 +23,10 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
|||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||
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 java.util.Currency
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
@ -58,6 +60,11 @@ class BackupsSettingsViewModel : ViewModel() {
|
|||
|
||||
private fun loadEnabledState() {
|
||||
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) {
|
||||
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
|
||||
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
|
||||
|
|
|
@ -10,7 +10,12 @@ import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
|||
/**
|
||||
* State container for BackupStatusData, including the enabled state.
|
||||
*/
|
||||
data class BackupRestoreState(
|
||||
val enabled: Boolean,
|
||||
val backupStatusData: BackupStatusData
|
||||
)
|
||||
sealed interface BackupRestoreState {
|
||||
data object None : BackupRestoreState
|
||||
data class Ready(
|
||||
val bytes: String
|
||||
) : BackupRestoreState
|
||||
data class FromBackupStatusData(
|
||||
val backupStatusData: BackupStatusData
|
||||
) : BackupRestoreState
|
||||
}
|
||||
|
|
|
@ -58,7 +58,9 @@ import androidx.compose.ui.res.dimensionResource
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
@ -99,6 +101,7 @@ import org.thoughtcrime.securesms.util.viewModel
|
|||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
@ -199,12 +202,20 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onStartMediaRestore() {
|
||||
// TODO - [backups] Begin media restore.
|
||||
}
|
||||
|
||||
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() {
|
||||
// TODO - [backups] Skip media restoration
|
||||
// TODO - [backups] Skip disk-full media restoration
|
||||
}
|
||||
|
||||
override fun onLearnMoreAboutLostSubscription() {
|
||||
|
@ -292,6 +303,8 @@ private interface ContentCallbacks {
|
|||
fun onSelectBackupsFrequencyChange(newFrequency: BackupFrequency) = Unit
|
||||
fun onTurnOffAndDeleteBackupsConfirm() = Unit
|
||||
fun onViewBackupKeyClick() = Unit
|
||||
fun onStartMediaRestore() = Unit
|
||||
fun onDisplaySkipMediaRestoreProtectionDialog() = Unit
|
||||
fun onSkipMediaRestore() = Unit
|
||||
fun onCancelMediaRestore() = Unit
|
||||
fun onRenewLostSubscription() = Unit
|
||||
|
@ -365,6 +378,30 @@ private fun RemoteBackupsSettingsContent(
|
|||
}
|
||||
|
||||
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(
|
||||
backupProgress = backupProgress,
|
||||
lastBackupTimestamp = lastBackupTimestamp,
|
||||
|
@ -374,7 +411,7 @@ private fun RemoteBackupsSettingsContent(
|
|||
contentCallbacks = contentCallbacks
|
||||
)
|
||||
} else {
|
||||
if (backupRestoreState.enabled) {
|
||||
if (backupRestoreState is BackupRestoreState.FromBackupStatusData) {
|
||||
item {
|
||||
BackupStatusRow(
|
||||
backupStatusData = backupRestoreState.backupStatusData,
|
||||
|
@ -443,6 +480,18 @@ private fun RemoteBackupsSettingsContent(
|
|||
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) {
|
||||
|
@ -931,8 +980,8 @@ private fun FailedToTurnOffBackupDialog(
|
|||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = "TODO",
|
||||
body = "TODO",
|
||||
title = stringResource(R.string.RemoteBackupsSettingsFragment__couldnt_turn_off_and_delete_backups),
|
||||
body = stringResource(R.string.RemoteBackupsSettingsFragment__a_network_error_occurred),
|
||||
confirm = stringResource(id = android.R.string.ok),
|
||||
onConfirm = {},
|
||||
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)
|
||||
@Composable
|
||||
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
|
||||
private fun getTextForFrequency(backupsFrequency: BackupFrequency): String {
|
||||
return when (backupsFrequency) {
|
||||
|
@ -1082,7 +1182,7 @@ private fun RemoteBackupsSettingsContentPreview() {
|
|||
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
|
||||
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
|
||||
@Composable
|
||||
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
|
||||
@Composable
|
||||
private fun CircularProgressDialogPreview() {
|
||||
|
|
|
@ -112,7 +112,8 @@ data class RemoteBackupsSettingsState(
|
|||
PROGRESS_SPINNER,
|
||||
DOWNLOADING_YOUR_BACKUP,
|
||||
TURN_OFF_FAILED,
|
||||
SUBSCRIPTION_NOT_FOUND
|
||||
SUBSCRIPTION_NOT_FOUND,
|
||||
SKIP_MEDIA_RESTORE_PROTECTION
|
||||
}
|
||||
|
||||
enum class Snackbar {
|
||||
|
|
|
@ -21,13 +21,13 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
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.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
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.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
|
||||
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner
|
||||
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>()
|
||||
|
||||
val state: StateFlow<RemoteBackupsSettingsState> = _state
|
||||
|
@ -86,8 +86,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||
if (restoreProgress.enabled) {
|
||||
Log.d(TAG, "Backup is being restored. Collecting updates.")
|
||||
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)
|
||||
|
@ -218,10 +222,12 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||
price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)),
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
|
||||
subscription.isCanceled -> RemoteBackupsSettingsState.BackupState.Canceled(
|
||||
messageBackupsType = type,
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
)
|
||||
|
||||
else -> RemoteBackupsSettingsState.BackupState.Inactive(
|
||||
messageBackupsType = type,
|
||||
renewalTime = subscription.endOfCurrentPeriod.seconds
|
||||
|
|
|
@ -7694,6 +7694,26 @@
|
|||
<string name="RemoteBackupsSettingsFragment__renew">Renew</string>
|
||||
<!-- Button label to learn more about why subscription disappeared -->
|
||||
<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 that’s 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 that’s 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 -->
|
||||
<!-- Displayed as a bottom sheet title -->
|
||||
|
|
Loading…
Add table
Reference in a new issue