From 945453cb81b874d10d2e90e680f8b16aa8d2ed8e Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 7 Nov 2025 13:52:37 -0500 Subject: [PATCH] Refactor backup creation failures, add case for file too large. --- .../securesms/backup/ArchiveUploadProgress.kt | 4 - .../securesms/backup/v2/BackupRepository.kt | 37 ++------ .../v2/ui/status/BackupCreateErrorRow.kt | 94 +++++++++---------- .../settings/app/AppSettingsViewModel.kt | 20 ++-- .../remote/RemoteBackupsSettingsFragment.kt | 10 +- .../remote/RemoteBackupsSettingsState.kt | 4 +- .../remote/RemoteBackupsSettingsViewModel.kt | 6 +- .../InternalBackupPlaygroundFragment.kt | 5 +- .../securesms/jobs/BackupMessagesJob.kt | 22 +++-- .../securesms/keyvalue/BackupValues.kt | 51 ++++++---- .../logsubmit/LogSectionRemoteBackups.kt | 76 +++++++-------- app/src/main/res/values/strings.xml | 2 + 12 files changed, 154 insertions(+), 177 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt index 314f529cb2..68ed715409 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/ArchiveUploadProgress.kt @@ -207,10 +207,6 @@ object ArchiveUploadProgress { resetState() } - fun onValidationFailure() { - resetState() - } - fun onMainBackupFileUploadFailure() { resetState() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 2ecc342b9b..216eb3aa65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -116,6 +116,7 @@ import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob import org.thoughtcrime.securesms.jobs.StorageForcePushJob import org.thoughtcrime.securesms.jobs.Svr2MirrorJob import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob +import org.thoughtcrime.securesms.keyvalue.BackupValues import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredentials import org.thoughtcrime.securesms.keyvalue.KeyValueStore import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -389,8 +390,8 @@ object BackupRepository { CancelRestoreMediaJob.enqueue() } - fun markBackupFailure() { - SignalStore.backup.markMessageBackupFailure() + fun markBackupCreationFailed(error: BackupValues.BackupCreationError) { + SignalStore.backup.markBackupCreationFailed(error) ArchiveUploadProgress.onMainBackupFileUploadFailure() if (!SignalStore.backup.hasBackupBeenUploaded) { @@ -416,7 +417,7 @@ object BackupRepository { } fun clearBackupFailure() { - SignalStore.backup.clearMessageBackupFailure() + SignalStore.backup.clearBackupCreationFailed() ServiceUtil.getNotificationManager(AppDependencies.application).cancel(NotificationIds.INITIAL_BACKUP_FAILED) } @@ -467,7 +468,7 @@ object BackupRepository { */ @JvmStatic fun shouldDisplayBackupFailedIndicator(): Boolean { - if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupFailure) { + if (shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupCreationError) { return false } @@ -482,30 +483,6 @@ object BackupRepository { return !(shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupAlreadyRedeemedError) } - /** - * 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.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 - } - /** * Displayed when the user falls out of the grace period for backups after their subscription * expires. @@ -554,7 +531,7 @@ object BackupRepository { return false } - return (!SignalStore.backup.hasBackupBeenUploaded || SignalStore.backup.hasValidationError) && SignalStore.backup.hasBackupFailure && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime + return SignalStore.backup.hasBackupCreationError && SignalStore.backup.backupCreationError != BackupValues.BackupCreationError.TRANSIENT && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime } /** @@ -675,7 +652,7 @@ object BackupRepository { } } - private fun shouldNotDisplayBackupFailedMessaging(): Boolean { + fun shouldNotDisplayBackupFailedMessaging(): Boolean { return !SignalStore.account.isRegistered || !SignalStore.backup.areBackupsEnabled } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreateErrorRow.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreateErrorRow.kt index a978fe02ff..2778c15537 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreateErrorRow.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupCreateErrorRow.kt @@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.backup.v2.ui.status import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -19,7 +21,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.AnnotatedString.Builder import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign @@ -32,6 +34,7 @@ import androidx.compose.ui.unit.sp import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Previews import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.keyvalue.BackupValues import org.signal.core.ui.R as CoreUiR private val YELLOW_DOT = Color(0xFFFFCC00) @@ -41,27 +44,18 @@ private val YELLOW_DOT = Color(0xFFFFCC00) */ @Composable fun BackupCreateErrorRow( - showCouldNotComplete: Boolean, - showBackupFailed: Boolean, + error: BackupValues.BackupCreationError, onLearnMoreClick: () -> Unit = {} ) { - if (showBackupFailed) { - val inlineContentMap = mapOf( - "yellow_bullet" to InlineTextContent( - Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter) - ) { - Box( - modifier = Modifier - .size(12.dp) - .background(color = YELLOW_DOT, shape = CircleShape) - ) + when (error) { + BackupValues.BackupCreationError.TRANSIENT -> { + BackupAlertText { + append(stringResource(R.string.BackupStatusRow__your_last_backup)) } - ) + } - BackupAlertText( - text = buildAnnotatedString { - appendInlineContent("yellow_bullet") - append(" ") + BackupValues.BackupCreationError.VALIDATION -> { + BackupAlertText { append(stringResource(R.string.BackupStatusRow__your_last_backup_latest_version)) append(" ") withLink( @@ -74,11 +68,29 @@ fun BackupCreateErrorRow( ) { append(stringResource(R.string.BackupStatusRow__learn_more)) } - }, - inlineContent = inlineContentMap - ) - } else if (showCouldNotComplete) { - val inlineContentMap = mapOf( + } + } + + BackupValues.BackupCreationError.BACKUP_FILE_TOO_LARGE -> { + BackupAlertText { + append(stringResource(R.string.BackupStatusRow__backup_file_too_large)) + } + } + } +} + +@Composable +private fun BackupAlertText(stringBuilder: @Composable Builder.() -> Unit) { + Text( + text = buildAnnotatedString { + appendInlineContent("yellow_bullet") + append(" ") + stringBuilder() + }, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)), + inlineContent = mapOf( "yellow_bullet" to InlineTextContent( Placeholder(20.sp, 12.sp, PlaceholderVerticalAlign.TextCenter) ) { @@ -89,26 +101,6 @@ fun BackupCreateErrorRow( ) } ) - - BackupAlertText( - text = buildAnnotatedString { - appendInlineContent("yellow_bullet") - append(" ") - append(stringResource(R.string.BackupStatusRow__your_last_backup)) - }, - inlineContent = inlineContentMap - ) - } -} - -@Composable -private fun BackupAlertText(text: AnnotatedString, inlineContent: Map) { - Text( - text = text, - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter)), - inlineContent = inlineContent ) } @@ -116,14 +108,12 @@ private fun BackupAlertText(text: AnnotatedString, inlineContent: Map BackupFailureState.NONE + SignalStore.backup.isNotEnoughRemoteStorageSpace -> BackupFailureState.OUT_OF_STORAGE_SPACE + SignalStore.backup.hasBackupCreationError -> BackupFailureState.COULD_NOT_COMPLETE_BACKUP + SignalStore.backup.subscriptionStateMismatchDetected -> BackupFailureState.SUBSCRIPTION_STATE_MISMATCH + SignalStore.backup.hasBackupAlreadyRedeemedError -> BackupFailureState.ALREADY_REDEEMED + else -> BackupFailureState.NONE } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt index e46a298708..4c9714a2ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt @@ -539,11 +539,10 @@ private fun RemoteBackupsSettingsContent( contentCallbacks = contentCallbacks ) } else { - if (state.showBackupCreateFailedError || state.showBackupCreateCouldNotCompleteError) { + if (state.backupCreationError != null) { item { BackupCreateErrorRow( - showCouldNotComplete = state.showBackupCreateCouldNotCompleteError, - showBackupFailed = state.showBackupCreateFailedError, + error = state.backupCreationError, onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure ) } @@ -888,11 +887,10 @@ private fun LazyListScope.appendBackupDetailsItems( } } - if (state.showBackupCreateFailedError || state.showBackupCreateCouldNotCompleteError) { + if (state.backupCreationError != null) { item { BackupCreateErrorRow( - showCouldNotComplete = state.showBackupCreateCouldNotCompleteError, - showBackupFailed = state.showBackupCreateFailedError, + error = state.backupCreationError, onLearnMoreClick = contentCallbacks::onLearnMoreAboutBackupFailure ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt index 607df0bfbe..c8eedca866 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.components.settings.app.backups.remote import org.signal.core.util.ByteSize import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.components.settings.app.backups.BackupState +import org.thoughtcrime.securesms.keyvalue.BackupValues /** * @param includeDebuglog The state for whether or not we should include a debuglog in the backup. If `null`, hide the setting. @@ -30,8 +31,7 @@ data class RemoteBackupsSettingsState( val includeDebuglog: Boolean? = null, val canBackupMessagesJobRun: Boolean = false, val backupMediaDetails: BackupMediaDetails? = null, - val showBackupCreateFailedError: Boolean = false, - val showBackupCreateCouldNotCompleteError: Boolean = false, + val backupCreationError: BackupValues.BackupCreationError? = null, val freeTierMediaRetentionDays: Int = -1, val isGooglePlayServicesAvailable: Boolean = false ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 8ef6cb3135..d52335138a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -77,8 +77,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { canBackUpUsingCellular = SignalStore.backup.backupWithCellular, canRestoreUsingCellular = SignalStore.backup.restoreWithCellular, includeDebuglog = SignalStore.internal.includeDebuglogInBackup.takeIf { RemoteConfig.internalUser }, - showBackupCreateFailedError = BackupRepository.shouldDisplayBackupFailedSettingsRow(), - showBackupCreateCouldNotCompleteError = BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow() + backupCreationError = SignalStore.backup.backupCreationError ) ) @@ -349,8 +348,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() { canRestoreUsingCellular = SignalStore.backup.restoreWithCellular, isOutOfStorageSpace = BackupRepository.shouldDisplayOutOfRemoteStorageSpaceUx(), hasRedemptionError = lastPurchase?.data?.error?.data_ == "409", - showBackupCreateFailedError = BackupRepository.shouldDisplayBackupFailedSettingsRow(), - showBackupCreateCouldNotCompleteError = BackupRepository.shouldDisplayCouldNotCompleteBackupSettingsRow() + backupCreationError = SignalStore.backup.backupCreationError ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt index 3ac8468fa4..6e5bc61c9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt @@ -82,6 +82,7 @@ import org.thoughtcrime.securesms.jobs.ArchiveAttachmentReconciliationJob import org.thoughtcrime.securesms.jobs.ArchiveThumbnailBackfillJob import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob import org.thoughtcrime.securesms.jobs.LocalBackupJob +import org.thoughtcrime.securesms.keyvalue.BackupValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.Util @@ -561,10 +562,10 @@ fun Screen( ) Rows.TextRow( - text = "Mark backup failure", + text = "Mark backup validation failure", label = "This will display the error sheet when returning to the chats list.", onClick = { - BackupRepository.markBackupFailure() + BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.VALIDATION) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt index 639ef35960..03edac5ded 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupMessagesJob.kt @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.jobmanager.impl.BackupMessagesConstraint import org.thoughtcrime.securesms.jobs.protos.BackupMessagesJobData +import org.thoughtcrime.securesms.keyvalue.BackupValues import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.isDecisionPending import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity @@ -120,6 +121,8 @@ class BackupMessagesJob private constructor( } } + private var backupErrorHandled = false + constructor() : this( syncTime = 0L, dataFile = "", @@ -145,9 +148,9 @@ class BackupMessagesJob private constructor( } override fun onFailure() { - if (!isCanceled) { + if (!isCanceled && !backupErrorHandled) { Log.w(TAG, "Failed to backup user messages. Marking failure state.", true) - BackupRepository.markBackupFailure() + BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.TRANSIENT) } } @@ -294,13 +297,15 @@ class BackupMessagesJob private constructor( Log.i(TAG, "Backup file is too large! Size: ${tempBackupFile.length()} bytes", result.getCause(), true) tempBackupFile.delete() this.dataFile = "" - // TODO [backup] Need to show the user an error + BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.BACKUP_FILE_TOO_LARGE) + backupErrorHandled = true + return Result.failure() } else -> { Log.i(TAG, "Status code failure", result.getCause(), true) + return Result.retry(defaultBackoff()) } } - return Result.retry(defaultBackoff()) } is NetworkResult.ApplicationError -> throw result.throwable @@ -458,7 +463,6 @@ class BackupMessagesJob private constructor( when (val result = ArchiveValidator.validateSignalBackup(tempBackupFile, backupKey, forwardSecrecyToken)) { ArchiveValidator.ValidationResult.Success -> { - SignalStore.backup.hasValidationError = false Log.d(TAG, "Successfully passed validation.", true) } @@ -471,8 +475,8 @@ class BackupMessagesJob private constructor( Log.w(TAG, "The backup file fails validation! Message: ${result.exception.message}, Details: ${result.messageDetails}", true) tempBackupFile.delete() this.dataFile = "" - SignalStore.backup.hasValidationError = true - ArchiveUploadProgress.onValidationFailure() + BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.VALIDATION) + backupErrorHandled = true return BackupFileResult.Failure } @@ -481,8 +485,8 @@ class BackupMessagesJob private constructor( tempBackupFile.delete() this.dataFile = "" AppDependencies.jobManager.add(E164FormattingJob()) - SignalStore.backup.hasValidationError = true - ArchiveUploadProgress.onValidationFailure() + BackupRepository.markBackupCreationFailed(BackupValues.BackupCreationError.VALIDATION) + backupErrorHandled = true return BackupFileResult.Failure } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt index 63a2768bdf..e6b1887299 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import okio.withLock +import org.signal.core.util.LongSerializer import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.backup.DeletionState import org.thoughtcrime.securesms.backup.RestoreState @@ -67,7 +68,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_BACKUP_UPLOADED = "backup.backupUploaded" private const val KEY_SUBSCRIPTION_STATE_MISMATCH = "backup.subscriptionStateMismatch" - private const val KEY_BACKUP_FAIL = "backup.failed" + private const val KEY_BACKUP_CREATION_ERROR = "backup.creationError" private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME = "backup.failed.acknowledged.snooze.time" private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_COUNT = "backup.failed.acknowledged.snooze.count" private const val KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME = "backup.failed.sheet.snooze" @@ -77,7 +78,6 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { private const val KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE = "backup.not.enough.remote.storage.space" private const val KEY_NOT_ENOUGH_REMOTE_STORAGE_SPACE_DISPLAY_SHEET = "backup.not.enough.remote.storage.space.display.sheet" private const val KEY_MANUAL_NO_BACKUP_NOTIFIED = "backup.manual.no.backup.notified" - private const val KEY_VALIDATION_ERROR = "backup.validation.error" private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore" private const val KEY_BACKUP_EXPIRED_AND_DOWNGRADED = "backup.expired.and.downgraded" @@ -259,7 +259,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { if (storedValue != value) { clearNotEnoughRemoteStorageSpace() - clearMessageBackupFailure() + clearBackupCreationFailed() clearMessageBackupFailureSheetWatermark() } @@ -302,10 +302,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { /** True if we believe we have successfully uploaded a backup, otherwise false. */ var hasBackupBeenUploaded: Boolean by booleanValue(KEY_BACKUP_UPLOADED, false) - /** Set when we fail to validate a user's backup during the export process */ - var hasValidationError: Boolean by booleanValue(KEY_VALIDATION_ERROR, false) - - val hasBackupFailure: Boolean get() = getBoolean(KEY_BACKUP_FAIL, false) + val hasBackupCreationError: Boolean get() = backupCreationError != null + val backupCreationError: BackupCreationError? by enumValue(KEY_BACKUP_CREATION_ERROR, null, BackupCreationError.serializer) val nextBackupFailureSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME, 0L).milliseconds val nextBackupFailureSheetSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, getNextBackupFailureSheetSnoozeTime(lastBackupTime.milliseconds).inWholeMilliseconds).milliseconds @@ -343,11 +341,6 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { backupDownloadNotifierState = null } - fun internalSetBackupFailedErrorState() { - markMessageBackupFailure() - putLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, 0) - } - /** * Call when the user disables backups. Clears/resets all relevant fields. */ @@ -458,9 +451,9 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { .apply() } - fun markMessageBackupFailure() { + fun markBackupCreationFailed(error: BackupCreationError) { store.beginWrite() - .putBoolean(KEY_BACKUP_FAIL, true) + .putLong(KEY_BACKUP_CREATION_ERROR, error.value) .putLong(KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME, System.currentTimeMillis()) .putLong(KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_COUNT, 0) .putLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, System.currentTimeMillis()) @@ -468,7 +461,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { } fun updateMessageBackupFailureWatermark() { - if (!hasBackupFailure) { + if (!hasBackupCreationError) { return } @@ -485,8 +478,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { .apply() } - fun clearMessageBackupFailure() { - putBoolean(KEY_BACKUP_FAIL, false) + fun clearBackupCreationFailed() { + putLong(KEY_BACKUP_CREATION_ERROR, -1) } fun updateMessageBackupFailureSheetWatermark() { @@ -587,4 +580,28 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) { putLong(cdnTimestampKey, System.currentTimeMillis()) } } + + enum class BackupCreationError(val value: Long) { + /** A temporary failure, usually cause by poor network. */ + TRANSIENT(1), + + /** The validation of the backup file failed. This likely cannot be fixed without an app update. */ + VALIDATION(2), + + /** The backup file itself is too large. The only resolution would be for the user to delete some number of messages. */ + BACKUP_FILE_TOO_LARGE(3); + + companion object { + + val serializer = object : LongSerializer { + override fun serialize(data: BackupCreationError?): Long { + return data?.value ?: -1 + } + + override fun deserialize(input: Long): BackupCreationError? { + return entries.firstOrNull { it.value == input } + } + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt index 60ba068ac6..ecbee515f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionRemoteBackups.kt @@ -27,22 +27,22 @@ class LogSectionRemoteBackups : LogSection { val output = StringBuilder() output.append("-- Backup State\n") - output.append("Enabled: ${SignalStore.backup.areBackupsEnabled}\n") - output.append("Current tier: ${SignalStore.backup.backupTier}\n") - output.append("Latest tier: ${SignalStore.backup.latestBackupTier}\n") - output.append("Backup override tier: ${SignalStore.backup.backupTierInternalOverride}\n") - output.append("Last backup time: ${SignalStore.backup.lastBackupTime}\n") - output.append("Last check-in: ${SignalStore.backup.lastCheckInMillis}\n") - output.append("Last reconciliation time: ${SignalStore.backup.lastAttachmentReconciliationTime}\n") - output.append("Days since last backup: ${SignalStore.backup.daysSinceLastBackup}\n") - output.append("User manually skipped media restore: ${SignalStore.backup.userManuallySkippedMediaRestore}\n") - output.append("Can backup with cellular: ${SignalStore.backup.backupWithCellular}\n") - output.append("Has backup been uploaded: ${SignalStore.backup.hasBackupBeenUploaded}\n") - output.append("Has backup failure: ${SignalStore.backup.hasBackupFailure}\n") - output.append("Optimize storage: ${SignalStore.backup.optimizeStorage}\n") + output.append("Enabled : ${SignalStore.backup.areBackupsEnabled}\n") + output.append("Current tier : ${SignalStore.backup.backupTier}\n") + output.append("Latest tier : ${SignalStore.backup.latestBackupTier}\n") + output.append("Backup override tier : ${SignalStore.backup.backupTierInternalOverride}\n") + output.append("Last backup time : ${SignalStore.backup.lastBackupTime}\n") + output.append("Last check-in : ${SignalStore.backup.lastCheckInMillis}\n") + output.append("Last reconciliation time : ${SignalStore.backup.lastAttachmentReconciliationTime}\n") + output.append("Days since last backup : ${SignalStore.backup.daysSinceLastBackup}\n") + output.append("User manually skipped media restore : ${SignalStore.backup.userManuallySkippedMediaRestore}\n") + output.append("Can backup with cellular : ${SignalStore.backup.backupWithCellular}\n") + output.append("Has backup been uploaded : ${SignalStore.backup.hasBackupBeenUploaded}\n") + output.append("Backup failure state : ${SignalStore.backup.backupCreationError?.name ?: "None"}\n") + output.append("Optimize storage : ${SignalStore.backup.optimizeStorage}\n") output.append("Detected subscription state mismatch: ${SignalStore.backup.subscriptionStateMismatchDetected}\n") - output.append("Last verified key time: ${SignalStore.backup.lastVerifyKeyTime}\n") - output.append("Restore state: ${ArchiveRestoreProgress.state}\n") + output.append("Last verified key time : ${SignalStore.backup.lastVerifyKeyTime}\n") + output.append("Restore state : ${ArchiveRestoreProgress.state}\n") output.append("\n -- Subscription State\n") val backupSubscriptionId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) @@ -50,21 +50,21 @@ class LogSectionRemoteBackups : LogSection { val googlePlayServicesAvailability = GooglePlayServicesAvailability.fromCode(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)) val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) - output.append("Has backup subscription id: ${backupSubscriptionId != null}\n") - output.append("Google Play Billing state: $googlePlayBillingAccess\n") - output.append("Google Play Services state: $googlePlayServicesAvailability\n\n") + output.append("Has backup subscription id : ${backupSubscriptionId != null}\n") + output.append("Google Play Billing state : $googlePlayBillingAccess\n") + output.append("Google Play Services state : $googlePlayServicesAvailability\n\n") if (inAppPayment != null) { - output.append("IAP end of period (seconds): ${inAppPayment.endOfPeriodSeconds}\n") - output.append("IAP state: ${inAppPayment.state.name}\n") - output.append("IAP inserted at (seconds): ${inAppPayment.insertedAt.inWholeSeconds}\n") - output.append("IAP updated at (seconds): ${inAppPayment.updatedAt.inWholeSeconds}\n") - output.append("IAP notified flag: ${inAppPayment.notified}\n") - output.append("IAP level: ${inAppPayment.data.level}\n") - output.append("IAP redemption stage (or null): ${inAppPayment.data.redemption?.stage}\n") - output.append("IAP error type (or null): ${inAppPayment.data.error?.type}\n") + output.append("IAP end of period (seconds) : ${inAppPayment.endOfPeriodSeconds}\n") + output.append("IAP state : ${inAppPayment.state.name}\n") + output.append("IAP inserted at (seconds) : ${inAppPayment.insertedAt.inWholeSeconds}\n") + output.append("IAP updated at (seconds) : ${inAppPayment.updatedAt.inWholeSeconds}\n") + output.append("IAP notified flag : ${inAppPayment.notified}\n") + output.append("IAP level : ${inAppPayment.data.level}\n") + output.append("IAP redemption stage (or null) : ${inAppPayment.data.redemption?.stage}\n") + output.append("IAP error type (or null) : ${inAppPayment.data.error?.type}\n") output.append("IAP cancellation reason (or null): ${inAppPayment.data.cancellation?.reason}\n") - output.append("IAP price: ${inAppPayment.data.amount?.toFiatMoney()?.let { FiatMoneyUtil.format(context.resources, it)} ?: "Not available" }\n") + output.append("IAP price : ${inAppPayment.data.amount?.toFiatMoney()?.let { FiatMoneyUtil.format(context.resources, it)} ?: "Not available" }\n") } else { output.append("No in-app payment data available.\n") } @@ -90,7 +90,7 @@ class LogSectionRemoteBackups : LogSection { output.append(SignalStore.backup.archiveUploadState!!.toPrettyString()) if (SignalStore.backup.archiveUploadState!!.state !in setOf(ArchiveUploadProgressState.State.None, ArchiveUploadProgressState.State.UserCanceled)) { - output.append("Pending bytes: ${SignalDatabase.attachments.getPendingArchiveUploadBytes()}\n") + output.append("Pending bytes : ${SignalDatabase.attachments.getPendingArchiveUploadBytes()}\n") } } else { output.append("None\n") @@ -105,28 +105,28 @@ class LogSectionRemoteBackups : LogSection { private fun ArchiveUploadProgressState.toPrettyString(): String { return buildString { - appendLine("state: ${state.name}") - appendLine("backupPhase: ${backupPhase.name}") - appendLine("frameExportCount: $frameExportCount") - appendLine("frameTotalCount: $frameTotalCount") + appendLine("state : ${state.name}") + appendLine("backupPhase : ${backupPhase.name}") + appendLine("frameExportCount : $frameExportCount") + appendLine("frameTotalCount : $frameTotalCount") appendLine("backupFileUploadedBytes: $backupFileUploadedBytes") - appendLine("backupFileTotalBytes: $backupFileTotalBytes") - appendLine("mediaUploadedBytes: $mediaUploadedBytes") - appendLine("mediaTotalBytes: $mediaTotalBytes") + appendLine("backupFileTotalBytes : $backupFileTotalBytes") + appendLine("mediaUploadedBytes : $mediaUploadedBytes") + appendLine("mediaTotalBytes : $mediaTotalBytes") if (frameTotalCount > 0) { val frameProgress = (frameExportCount.toDouble() / frameTotalCount * 100).toInt() - appendLine("Frame export progress: $frameProgress%") + appendLine("Frame export progress : $frameProgress%") } if (backupFileTotalBytes > 0) { val backupFileProgress = (backupFileUploadedBytes.toDouble() / backupFileTotalBytes * 100).toInt() - appendLine("Backup file upload progress: $backupFileProgress%") + appendLine("Backup file upload progress : $backupFileProgress%") } if (mediaTotalBytes > 0) { val mediaProgress = (mediaUploadedBytes.toDouble() / mediaTotalBytes * 100).toInt() - appendLine("Media upload progress: $mediaProgress%") + appendLine("Media upload progress : $mediaProgress%") } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1367627ea6..a1abe9c4d6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8050,6 +8050,8 @@ Your last backup couldn\'t be completed. Make sure your phone is connected to Wi-Fi and tap \"Back up now\" to try again. Your last backup couldn\'t be completed. Make sure you\'re on the latest version of Signal and try again. + + The number of messages you have exceeds the storage limit. Delete some messages and try again. Learn more