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.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

View file

@ -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() {

View file

@ -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.
*/

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.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()

View file

@ -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
}

View file

@ -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() {

View file

@ -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 {

View file

@ -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

View file

@ -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 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 -->
<!-- Displayed as a bottom sheet title -->