Add validation error UI.

This commit is contained in:
Michelle Tang 2024-12-02 09:07:46 -08:00 committed by Greyson Parrelli
parent 756262c1fe
commit 3c086f347e
13 changed files with 191 additions and 22 deletions

View file

@ -104,6 +104,10 @@ object ArchiveUploadProgress {
updateState(PROGRESS_NONE)
}
fun onValidationFailure() {
updateState(PROGRESS_NONE)
}
private fun updateState(state: ArchiveUploadProgressState, notify: Boolean = true) {
uploadProgress = state
SignalStore.backup.archiveUploadState = state

View file

@ -204,14 +204,27 @@ object BackupRepository {
}
/**
* Whether the "Could not complete backup" row should be displayed in settings.
* Whether the "Backup Failed" row should be displayed in settings.
* Shown when the initial backup creation has failed
*/
fun shouldDisplayBackupFailedSettingsRow(): Boolean {
if (shouldNotDisplayBackupFailedMessaging()) {
return false
}
return SignalStore.backup.hasBackupFailure
return !SignalStore.backup.hasBackupBeenUploaded && SignalStore.backup.hasBackupFailure
}
/**
* Whether the "Could not complete backup" row should be displayed in settings.
* Shown when a new backup could not be created but there is an existing one already
*/
fun shouldDisplayCouldNotCompleteBackupSettingsRow(): Boolean {
if (shouldNotDisplayBackupFailedMessaging()) {
return false
}
return SignalStore.backup.hasBackupBeenUploaded && SignalStore.backup.hasBackupFailure
}
/**
@ -230,7 +243,8 @@ object BackupRepository {
}
/**
* Whether or not the "Could not complete backup" sheet should be displayed.
* Whether or not the "Backup failed" sheet should be displayed.
* Should only be displayed if this is the failure of the initial backup creation.
*/
@JvmStatic
fun shouldDisplayBackupFailedSheet(): Boolean {
@ -238,7 +252,19 @@ object BackupRepository {
return false
}
return System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime
return !SignalStore.backup.hasBackupBeenUploaded && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime
}
/**
* Whether or not the "Could not complete backup" sheet should be displayed.
*/
@JvmStatic
fun shouldDisplayCouldNotCompleteBackupSheet(): Boolean {
if (shouldNotDisplayBackupFailedMessaging()) {
return false
}
return SignalStore.backup.hasBackupBeenUploaded && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime
}
fun snoozeYourMediaWillBeDeletedTodaySheet() {
@ -249,7 +275,7 @@ object BackupRepository {
* Whether or not the "Your media will be deleted today" sheet should be displayed.
*/
suspend fun shouldDisplayYourMediaWillBeDeletedTodaySheet(): Boolean {
if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.optimizeStorage) {
if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupBeenUploaded || !SignalStore.backup.optimizeStorage) {
return false
}
@ -285,7 +311,7 @@ object BackupRepository {
}
private fun shouldNotDisplayBackupFailedMessaging(): Boolean {
return !RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled || !SignalStore.backup.hasBackupBeenUploaded
return !RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled
}
/**

View file

@ -57,6 +57,8 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.PlayStoreUtil
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@ -67,6 +69,8 @@ import org.signal.core.ui.R as CoreUiR
*/
class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
override val peekHeightPercentage: Float = 0.75f
companion object {
private const val ARG_ALERT = "alert"
@ -126,6 +130,8 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
}
is BackupAlert.DiskFull -> Unit
is BackupAlert.BackupFailed ->
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
}
dismissAllowingStateLoss()
@ -144,6 +150,8 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
is BackupAlert.DiskFull -> {
displaySkipRestoreDialog()
}
// TODO [backups] - Update support URL with backups page
BackupAlert.BackupFailed -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_support_url))
}
dismissAllowingStateLoss()
@ -153,7 +161,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
super.onDismiss(dialog)
when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> BackupRepository.markBackupFailedSheetDismissed()
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed -> BackupRepository.markBackupFailedSheetDismissed()
is BackupAlert.MediaWillBeDeletedToday -> BackupRepository.snoozeYourMediaWillBeDeletedTodaySheet()
else -> Unit
}
@ -261,6 +269,7 @@ private fun BackupAlertSheetContent(
is BackupAlert.MediaBackupsAreOff -> MediaBackupsAreOffBody(backupAlert.endOfPeriodSeconds, mediaTtl)
BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
BackupAlert.BackupFailed -> BackupFailedBody()
}
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
@ -366,12 +375,22 @@ private fun DiskFullBody(requiredSpace: String) {
)
}
@Composable
private fun BackupFailedBody() {
Text(
text = stringResource(id = R.string.BackupAlertBottomSheet__an_error_occurred),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 36.dp)
)
}
@Composable
private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.")
is BackupAlert.CouldNotCompleteBackup, is BackupAlert.DiskFull -> BackupsIconColors.Warning
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed, is BackupAlert.DiskFull -> BackupsIconColors.Warning
BackupAlert.MediaWillBeDeletedToday -> BackupsIconColors.Error
}
}
@ -385,6 +404,7 @@ private fun titleString(backupAlert: BackupAlert): String {
is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_expired)
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today)
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__backup_failed)
}
}
@ -399,6 +419,7 @@ private fun primaryActionString(
is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth)
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it)
is BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__check_for_update)
}
}
@ -411,6 +432,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
is BackupAlert.MediaBackupsAreOff -> R.string.BackupAlertBottomSheet__not_now
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__learn_more
}
}
}
@ -471,6 +493,17 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewBackupFailed() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.BackupFailed,
mediaTtl = 60.days
)
}
}
/**
* All necessary information to display the sheet should be handed in through the specific alert.
*/
@ -485,6 +518,12 @@ sealed class BackupAlert : Parcelable {
val daysSinceLastBackup: Int
) : BackupAlert()
/**
* This value is driven by the same watermarking system for [CouldNotCompleteBackup] so that only one of these sheets is shown by the system
* This value is driven by failure to complete the initial backup.
*/
data object BackupFailed : BackupAlert()
/**
* This value is driven by InAppPayment state, and will be automatically cleared when the sheet is displayed.
*/

View file

@ -22,11 +22,11 @@ object BackupAlertDelegate {
lifecycle.coroutineScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
if (BackupRepository.shouldDisplayBackupFailedSheet()) {
BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(fragmentManager, null)
} else if (BackupRepository.shouldDisplayCouldNotCompleteBackupSheet()) {
BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = SignalStore.backup.daysSinceLastBackup)).show(fragmentManager, null)
}
if (BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet()) {
BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday)
} else if (BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet()) {
BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday).show(fragmentManager, null)
}
}
}

View file

@ -195,6 +195,12 @@ fun BackupStatusBannerPreview() {
BackupStatusBanner(
data = BackupStatusData.CouldNotCompleteBackup
)
HorizontalDivider()
BackupStatusBanner(
data = BackupStatusData.BackupFailed
)
}
}
}
@ -235,6 +241,19 @@ sealed interface BackupStatusData {
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
}
/**
* Initial backup creation failure
*/
data object BackupFailed : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_error_24
override val title: String
@Composable
get() = stringResource(androidx.biometric.R.string.default_error_msg)
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
}
/**
* User does not have enough space on their device to complete backup restoration
*/

View file

@ -20,17 +20,19 @@ 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
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.Previews
@ -48,10 +50,13 @@ import org.signal.core.ui.R as CoreUiR
fun BackupStatusRow(
backupStatusData: BackupStatusData,
onSkipClick: () -> Unit = {},
onCancelClick: () -> Unit = {}
onCancelClick: () -> Unit = {},
onLearnMoreClick: () -> Unit = {}
) {
Column {
if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup) {
if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup &&
backupStatusData !is BackupStatusData.BackupFailed
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
@ -120,6 +125,40 @@ fun BackupStatusRow(
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
)
}
BackupStatusData.BackupFailed -> {
val inlineContentMap = mapOf(
"yellow_bullet" to InlineTextContent(
Placeholder(12.sp, 12.sp, PlaceholderVerticalAlign.TextCenter)
) {
Box(
modifier = Modifier
.size(12.dp)
.background(color = backupStatusData.iconColors.foreground, shape = CircleShape)
)
}
)
Text(
text = buildAnnotatedString {
appendInlineContent("yellow_bullet")
append(" ")
append(stringResource(R.string.BackupStatusRow__your_last_backup_latest_version))
append(" ")
withLink(
LinkAnnotation.Clickable(
stringResource(R.string.BackupStatusRow__learn_more),
styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.primary))
) {
onLearnMoreClick()
}
) {
append(stringResource(R.string.BackupStatusRow__learn_more))
}
},
inlineContent = inlineContentMap,
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
)
}
}
}
}
@ -241,3 +280,13 @@ fun BackupStatusRowCouldNotCompleteBackupPreview() {
)
}
}
@SignalPreview
@Composable
fun BackupStatusRowBackupFailedPreview() {
Previews.Preview {
BackupStatusRow(
backupStatusData = BackupStatusData.BackupFailed
)
}
}

View file

@ -215,7 +215,7 @@ private fun AppSettingsContent(
}
}
BackupFailureState.COULD_NOT_COMPLETE_BACKUP -> {
BackupFailureState.BACKUP_FAILED, BackupFailureState.COULD_NOT_COMPLETE_BACKUP -> {
item {
Dividers.Default()

View file

@ -72,6 +72,8 @@ class AppSettingsViewModel : ViewModel() {
private fun getBackupFailureState(): BackupFailureState {
return if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
BackupFailureState.BACKUP_FAILED
} else if (BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()) {
BackupFailureState.COULD_NOT_COMPLETE_BACKUP
} else if (SignalStore.backup.subscriptionStateMismatchDetected) {
BackupFailureState.SUBSCRIPTION_STATE_MISMATCH

View file

@ -10,6 +10,7 @@ package org.thoughtcrime.securesms.components.settings.app
*/
enum class BackupFailureState {
NONE,
BACKUP_FAILED,
COULD_NOT_COMPLETE_BACKUP,
SUBSCRIPTION_STATE_MISMATCH
}

View file

@ -83,6 +83,8 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.ArchiveUploadProgress
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusRow
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
@ -233,6 +235,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
requireActivity().finish()
requireActivity().startActivity(AppSettingsActivity.help(requireContext(), HelpFragment.REMOTE_BACKUPS_INDEX))
}
override fun onLearnMoreAboutBackupFailure() {
BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(parentFragmentManager, null)
}
}
private fun displayBackupKey() {
@ -314,6 +320,7 @@ private interface ContentCallbacks {
fun onRenewLostSubscription() = Unit
fun onLearnMoreAboutLostSubscription() = Unit
fun onContactSupport() = Unit
fun onLearnMoreAboutBackupFailure() = Unit
}
@Composable
@ -392,7 +399,8 @@ private fun RemoteBackupsSettingsContent(
BackupStatusRow(
backupStatusData = backupRestoreState.backupStatusData,
onCancelClick = contentCallbacks::onCancelMediaRestore,
onSkipClick = contentCallbacks::onSkipMediaRestore
onSkipClick = contentCallbacks::onSkipMediaRestore,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
)
}
} else if (backupRestoreState is BackupRestoreState.Ready && backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
@ -420,7 +428,8 @@ private fun RemoteBackupsSettingsContent(
BackupStatusRow(
backupStatusData = backupRestoreState.backupStatusData,
onCancelClick = contentCallbacks::onCancelMediaRestore,
onSkipClick = contentCallbacks::onSkipMediaRestore
onSkipClick = contentCallbacks::onSkipMediaRestore,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure
)
}
}
@ -920,8 +929,14 @@ private fun InProgressBackupRow(
)
}
val inProgressText = if (totalProgress == null || totalProgress == 0) {
stringResource(R.string.RemoteBackupsSettingsFragment__processing_backup)
} else {
stringResource(R.string.RemoteBackupsSettingsFragment__d_slash_d, progress ?: 0, totalProgress)
}
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__d_slash_d, progress ?: 0, totalProgress ?: 0),
text = inProgressText,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

View file

@ -92,6 +92,8 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
_restoreState.update { BackupRestoreState.FromBackupStatusData(BackupStatusData.BackupFailed) }
} else if (BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow()) {
_restoreState.update { BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup) }
} else {
_restoreState.update { BackupRestoreState.None }

View file

@ -101,8 +101,8 @@ class BackupMessagesJob private constructor(parameters: Parameters) : Job(parame
return Result.retry(defaultBackoff())
}
is ArchiveValidator.ValidationResult.ValidationError -> {
// TODO [backup] UX
Log.w(TAG, "The backup file fails validation! Message: " + result.exception.message)
ArchiveUploadProgress.onValidationFailure()
return Result.failure()
}
}

View file

@ -7504,6 +7504,12 @@
<string name="BackupAlertBottomSheet__skip_restore_question">Skip restore?</string>
<!-- Dialog text for skipping media restore -->
<string name="BackupAlertBottomSheet__if_you_skip_restore_the">If you skip restore the remaining media and attachments in your backup can be downloaded at a later time when storage space becomes available.</string>
<!-- Dialog title when a backup fails to be created -->
<string name="BackupAlertBottomSheet__backup_failed">Backup failed</string>
<!-- Dialog text for when a backup fails to be created and ways to fix it -->
<string name="BackupAlertBottomSheet__an_error_occurred">An error occurred and your backup could not be completed. Make sure you\'re on the latest version of Signal and try again. If this problem persists, contact support.</string>
<!-- Dialog action button that will allow you to check for any Signal version updates -->
<string name="BackupAlertBottomSheet__check_for_update">Check for update</string>
<!-- BackupStatus -->
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. -->
@ -7536,7 +7542,11 @@
<!-- Text row label to skip download -->
<string name="BackupStatusRow__skip_download">Skip download</string>
<!-- Text displayed when a backup could not be completed -->
<string name="BackupStatusRow__your_last_backup">Your last backup couldn\'t be completed. Make sure your phone is connected to Wi-F and tap "Back up now" to try again.</string>
<string name="BackupStatusRow__your_last_backup">Your last backup couldn\'t be completed. Make sure your phone is connected to Wi-Fi and tap \"Back up now\" to try again.</string>
<!-- Text displayed when a backup could not be completed and to check that they are on the latest version of Signal -->
<string name="BackupStatusRow__your_last_backup_latest_version">Your last backup couldn\'t be completed. Make sure you\'re on the latest version of Signal and try again.</string>
<!-- Text displayed in a row to learn more about why a backup failed -->
<string name="BackupStatusRow__learn_more">Learn more</string>
<!-- BackupsTypeSettingsFragment -->
<!-- Displayed as the user\'s payment method as a label in a preference row -->
@ -7740,6 +7750,8 @@
<string name="RemoteBackupsSettingsFragment__renew">Renew</string>
<!-- Button label to learn more about why subscription disappeared -->
<string name="RemoteBackupsSettingsFragment__learn_more">Learn more</string>
<!-- Linear progress dialog text shown when creating a backup -->
<string name="RemoteBackupsSettingsFragment__processing_backup">Processing backup…</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>