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) updateState(PROGRESS_NONE)
} }
fun onValidationFailure() {
updateState(PROGRESS_NONE)
}
private fun updateState(state: ArchiveUploadProgressState, notify: Boolean = true) { private fun updateState(state: ArchiveUploadProgressState, notify: Boolean = true) {
uploadProgress = state uploadProgress = state
SignalStore.backup.archiveUploadState = 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 { fun shouldDisplayBackupFailedSettingsRow(): Boolean {
if (shouldNotDisplayBackupFailedMessaging()) { if (shouldNotDisplayBackupFailedMessaging()) {
return false 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 @JvmStatic
fun shouldDisplayBackupFailedSheet(): Boolean { fun shouldDisplayBackupFailedSheet(): Boolean {
@ -238,7 +252,19 @@ object BackupRepository {
return false 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() { fun snoozeYourMediaWillBeDeletedTodaySheet() {
@ -249,7 +275,7 @@ object BackupRepository {
* Whether or not the "Your media will be deleted today" sheet should be displayed. * Whether or not the "Your media will be deleted today" sheet should be displayed.
*/ */
suspend fun shouldDisplayYourMediaWillBeDeletedTodaySheet(): Boolean { suspend fun shouldDisplayYourMediaWillBeDeletedTodaySheet(): Boolean {
if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.optimizeStorage) { if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupBeenUploaded || !SignalStore.backup.optimizeStorage) {
return false return false
} }
@ -285,7 +311,7 @@ object BackupRepository {
} }
private fun shouldNotDisplayBackupFailedMessaging(): Boolean { 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.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.payments.FiatMoneyUtil 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
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -67,6 +69,8 @@ import org.signal.core.ui.R as CoreUiR
*/ */
class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() { class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
override val peekHeightPercentage: Float = 0.75f
companion object { companion object {
private const val ARG_ALERT = "alert" private const val ARG_ALERT = "alert"
@ -126,6 +130,8 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
} }
is BackupAlert.DiskFull -> Unit is BackupAlert.DiskFull -> Unit
is BackupAlert.BackupFailed ->
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
} }
dismissAllowingStateLoss() dismissAllowingStateLoss()
@ -144,6 +150,8 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
is BackupAlert.DiskFull -> { is BackupAlert.DiskFull -> {
displaySkipRestoreDialog() displaySkipRestoreDialog()
} }
// TODO [backups] - Update support URL with backups page
BackupAlert.BackupFailed -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_support_url))
} }
dismissAllowingStateLoss() dismissAllowingStateLoss()
@ -153,7 +161,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
super.onDismiss(dialog) super.onDismiss(dialog)
when (backupAlert) { when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> BackupRepository.markBackupFailedSheetDismissed() is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed -> BackupRepository.markBackupFailedSheetDismissed()
is BackupAlert.MediaWillBeDeletedToday -> BackupRepository.snoozeYourMediaWillBeDeletedTodaySheet() is BackupAlert.MediaWillBeDeletedToday -> BackupRepository.snoozeYourMediaWillBeDeletedTodaySheet()
else -> Unit else -> Unit
} }
@ -261,6 +269,7 @@ private fun BackupAlertSheetContent(
is BackupAlert.MediaBackupsAreOff -> MediaBackupsAreOffBody(backupAlert.endOfPeriodSeconds, mediaTtl) is BackupAlert.MediaBackupsAreOff -> MediaBackupsAreOffBody(backupAlert.endOfPeriodSeconds, mediaTtl)
BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody() BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace) is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
BackupAlert.BackupFailed -> BackupFailedBody()
} }
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert) 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 @Composable
private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors { private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors {
return remember(backupAlert) { return remember(backupAlert) {
when (backupAlert) { when (backupAlert) {
BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.") 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 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) is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_expired)
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today) 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) 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) is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth)
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now) BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it) 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 is BackupAlert.MediaBackupsAreOff -> R.string.BackupAlertBottomSheet__not_now
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore 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. * 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 val daysSinceLastBackup: Int
) : BackupAlert() ) : 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. * 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.coroutineScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
if (BackupRepository.shouldDisplayBackupFailedSheet()) { 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) BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = SignalStore.backup.daysSinceLastBackup)).show(fragmentManager, null)
} } else if (BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet()) {
BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday).show(fragmentManager, null)
if (BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet()) {
BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday)
} }
} }
} }

View file

@ -195,6 +195,12 @@ fun BackupStatusBannerPreview() {
BackupStatusBanner( BackupStatusBanner(
data = BackupStatusData.CouldNotCompleteBackup data = BackupStatusData.CouldNotCompleteBackup
) )
HorizontalDivider()
BackupStatusBanner(
data = BackupStatusData.BackupFailed
)
} }
} }
} }
@ -235,6 +241,19 @@ sealed interface BackupStatusData {
override val iconColors: BackupsIconColors = BackupsIconColors.Warning 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 * 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.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
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign 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.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.signal.core.ui.Previews import org.signal.core.ui.Previews
@ -48,10 +50,13 @@ import org.signal.core.ui.R as CoreUiR
fun BackupStatusRow( fun BackupStatusRow(
backupStatusData: BackupStatusData, backupStatusData: BackupStatusData,
onSkipClick: () -> Unit = {}, onSkipClick: () -> Unit = {},
onCancelClick: () -> Unit = {} onCancelClick: () -> Unit = {},
onLearnMoreClick: () -> Unit = {}
) { ) {
Column { Column {
if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup) { if (backupStatusData !is BackupStatusData.CouldNotCompleteBackup &&
backupStatusData !is BackupStatusData.BackupFailed
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)) modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
@ -120,6 +125,40 @@ fun BackupStatusRow(
modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)) 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 { item {
Dividers.Default() Dividers.Default()

View file

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

View file

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

View file

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

View file

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

View file

@ -7504,6 +7504,12 @@
<string name="BackupAlertBottomSheet__skip_restore_question">Skip restore?</string> <string name="BackupAlertBottomSheet__skip_restore_question">Skip restore?</string>
<!-- Dialog text for skipping media restore --> <!-- 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> <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 --> <!-- BackupStatus -->
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. --> <!-- 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 --> <!-- Text row label to skip download -->
<string name="BackupStatusRow__skip_download">Skip download</string> <string name="BackupStatusRow__skip_download">Skip download</string>
<!-- Text displayed when a backup could not be completed --> <!-- 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 --> <!-- BackupsTypeSettingsFragment -->
<!-- Displayed as the user\'s payment method as a label in a preference row --> <!-- Displayed as the user\'s payment method as a label in a preference row -->
@ -7740,6 +7750,8 @@
<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>
<!-- 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 --> <!-- 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"> <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="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>