Surface error when local backup restore directory becomes inaccessible.

This commit is contained in:
Alex Hart
2026-04-02 11:27:22 -03:00
committed by GitHub
parent 01d1769e4c
commit 265f71dff3
11 changed files with 151 additions and 10 deletions

View File

@@ -96,6 +96,8 @@ import org.signal.core.util.logging.Log
import org.signal.donations.StripeApi
import org.signal.mediasend.MediaSendActivityContract
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
import org.thoughtcrime.securesms.backup.v2.ui.CouldNotCompleteBackupRestoreSheet
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.log.CallLogFilter
@@ -342,6 +344,19 @@ class MainActivity :
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
ArchiveRestoreProgress
.stateFlow
.filter { it.restoreStatus == ArchiveRestoreProgressState.RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE }
.collect {
ArchiveRestoreProgress.clearLocalRestoreDirectoryError()
CouldNotCompleteBackupRestoreSheet().show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
Log.i(TAG, "Local restore directory became unavailable.")
}
}
}
}
supportFragmentManager.setFragmentResultListener(

View File

@@ -157,6 +157,11 @@ object ArchiveRestoreProgress {
update()
}
fun clearLocalRestoreDirectoryError() {
SignalStore.backup.localRestoreDirectoryError = false
update()
}
fun clearFinishedStatus() {
store.update { state ->
if (state.restoreStatus == ArchiveRestoreProgressState.RestoreStatus.FINISHED) {
@@ -193,7 +198,11 @@ object ArchiveRestoreProgress {
!NetworkConstraint.isMet(AppDependencies.application) -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_INTERNET
!BatteryNotLowConstraint.isMet() -> ArchiveRestoreProgressState.RestoreStatus.LOW_BATTERY
!DiskSpaceNotLowConstraint.isMet() -> ArchiveRestoreProgressState.RestoreStatus.NOT_ENOUGH_DISK_SPACE
restoreState == RestoreState.NONE -> if (state.hasActivelyRestoredThisRun) ArchiveRestoreProgressState.RestoreStatus.FINISHED else ArchiveRestoreProgressState.RestoreStatus.NONE
restoreState == RestoreState.NONE -> when {
SignalStore.backup.localRestoreDirectoryError -> ArchiveRestoreProgressState.RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE
state.hasActivelyRestoredThisRun -> ArchiveRestoreProgressState.RestoreStatus.FINISHED
else -> ArchiveRestoreProgressState.RestoreStatus.NONE
}
else -> {
val availableBytes = SignalStore.backup.spaceAvailableOnDiskBytes

View File

@@ -69,6 +69,7 @@ data class ArchiveRestoreProgressState(
WAITING_FOR_INTERNET,
WAITING_FOR_WIFI,
NOT_ENOUGH_DISK_SPACE,
FINISHED
FINISHED,
LOCAL_RESTORE_DIRECTORY_UNAVAILABLE
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.CommunicationActions
/**
* Sheet displayed when the user's backup restoration failed during media import. Generally due
* to the files no longer being available.
*/
class CouldNotCompleteBackupRestoreSheet : ComposeBottomSheetDialogFragment() {
@Composable
override fun SheetContent() {
CouldNotCompleteBackupRestoreSheetContent(
onOkClick = { dismiss() },
onLearnMoreClick = {
dismiss()
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.backup_support_url))
}
)
}
}
@Composable
private fun CouldNotCompleteBackupRestoreSheetContent(
onOkClick: () -> Unit = {},
onLearnMoreClick: () -> Unit = {}
) {
val ok = stringResource(android.R.string.ok)
val primaryActionButtonState: BackupAlertActionButtonState = remember(ok, onOkClick) {
BackupAlertActionButtonState(
label = ok,
callback = onOkClick
)
}
val learnMore = stringResource(R.string.preferences__app_icon_learn_more)
val secondaryActionButtonState: BackupAlertActionButtonState = remember(learnMore, onLearnMoreClick) {
BackupAlertActionButtonState(
label = learnMore,
callback = onLearnMoreClick
)
}
BackupAlertBottomSheetContainer(
icon = {
BackupAlertIcon(iconColors = BackupsIconColors.Error)
},
title = stringResource(R.string.CouldNotCompleteBackupRestoreSheet__title),
primaryActionButtonState = primaryActionButtonState,
secondaryActionButtonState = secondaryActionButtonState
) {
Text(
text = stringResource(R.string.CouldNotCompleteBackupRestoreSheet__body_error)
)
Text(
text = stringResource(R.string.CouldNotCompleteBackupRestoreSheet__body_retry)
)
}
}
@DayNightPreviews
@Composable
private fun CouldNotCompleteBackupRestoreSheetContentPreview() {
Previews.BottomSheetContentPreview {
CouldNotCompleteBackupRestoreSheetContent()
}
}

View File

@@ -164,10 +164,10 @@ private fun ArchiveRestoreProgressState.iconResource(): Int {
RestoreStatus.WAITING_FOR_INTERNET,
RestoreStatus.WAITING_FOR_WIFI,
RestoreStatus.LOW_BATTERY -> R.drawable.symbol_backup_light
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> R.drawable.symbol_backup_error_24
RestoreStatus.FINISHED -> CoreUiR.drawable.symbol_check_circle_24
RestoreStatus.NONE -> throw IllegalStateException()
RestoreStatus.NONE,
RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE -> throw IllegalStateException()
}
}
@@ -199,7 +199,8 @@ private fun ArchiveRestoreProgressState.iconColor(): Color {
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> BackupsIconColors.Warning.foreground
RestoreStatus.FINISHED -> BackupsIconColors.Success.foreground
RestoreStatus.NONE -> throw IllegalStateException()
RestoreStatus.NONE,
RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE -> throw IllegalStateException()
}
}
@@ -233,7 +234,8 @@ private fun ArchiveRestoreProgressState.title(): String {
}
RestoreStatus.FINISHED -> stringResource(R.string.BackupStatus__restore_complete)
RestoreStatus.NONE -> throw IllegalStateException()
RestoreStatus.NONE,
RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE -> throw IllegalStateException()
}
}
@@ -277,7 +279,8 @@ private fun ArchiveRestoreProgressState.status(): String? {
RestoreStatus.LOW_BATTERY -> stringResource(R.string.BackupStatus__status_device_has_low_battery)
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> null
RestoreStatus.FINISHED -> this.totalToRestoreThisRun.toUnitString()
RestoreStatus.NONE -> throw IllegalStateException()
RestoreStatus.NONE,
RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE -> throw IllegalStateException()
}
}

View File

@@ -217,7 +217,8 @@ private fun progressColor(backupStatusData: ArchiveRestoreProgressState): Color
RestoreStatus.LOW_BATTERY,
RestoreStatus.NOT_ENOUGH_DISK_SPACE -> BackupsIconColors.Warning.foreground
RestoreStatus.FINISHED -> BackupsIconColors.Success.foreground
RestoreStatus.NONE -> BackupsIconColors.Normal.foreground
RestoreStatus.NONE,
RestoreStatus.LOCAL_RESTORE_DIRECTORY_UNAVAILABLE -> BackupsIconColors.Normal.foreground
}
}

View File

@@ -73,6 +73,7 @@ import org.signal.core.util.Util
import org.signal.core.util.getLength
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.backup.isIdle
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlertBottomSheet
@@ -270,6 +271,10 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
.setNegativeButton("Cancel", null)
.show()
},
onTriggerLocalRestoreDirectoryError = {
SignalStore.backup.localRestoreDirectoryError = true
ArchiveRestoreProgress.forceUpdate()
},
onDisplayInitialBackupFailureSheet = {
BackupRepository.displayInitialBackupFailureNotification()
BackupAlertBottomSheet
@@ -366,6 +371,7 @@ fun Screen(
onImportEncryptedBackupFromDiskConfirmed: (aci: String, backupKey: String) -> Unit = { _, _ -> },
onClearLocalMediaBackupState: () -> Unit = {},
onDeleteRemoteBackup: () -> Unit = {},
onTriggerLocalRestoreDirectoryError: () -> Unit = {},
onDisplayInitialBackupFailureSheet: () -> Unit = {}
) {
val context = LocalContext.current
@@ -584,6 +590,12 @@ fun Screen(
onClick = onClearLocalMediaBackupState
)
Rows.TextRow(
text = "Trigger local restore directory error",
label = "Simulates the restore directory becoming inaccessible during a local backup restore.",
onClick = onTriggerLocalRestoreDirectoryError
)
Dividers.Default()
Rows.TextRow(

View File

@@ -6,9 +6,11 @@ package org.thoughtcrime.securesms.jobs
import android.net.Uri
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.protos.LocalBackupRestoreMediaJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import java.io.File
/**
@@ -46,6 +48,7 @@ class LocalBackupRestoreMediaJob private constructor(
val archiveFileSystem = when (backupDirectoryUri.scheme) {
"content" -> ArchiveFileSystem.openForRestore(context, backupDirectoryUri) ?: run {
Log.w(TAG, "Unable to open backup directory: $backupDirectoryUri")
SignalStore.backup.localRestoreDirectoryError = true
return Result.failure()
}
else -> ArchiveFileSystem.fromFile(context, File(backupDirectoryUri.path!!))
@@ -56,7 +59,11 @@ class LocalBackupRestoreMediaJob private constructor(
return Result.success()
}
override fun onFailure() = Unit
override fun onFailure() {
ArchiveRestoreProgress.allMediaRestored()
// forceUpdate in case restoreState was already NONE and allMediaRestored() skipped its update()
ArchiveRestoreProgress.forceUpdate()
}
class Factory : Job.Factory<LocalBackupRestoreMediaJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): LocalBackupRestoreMediaJob {

View File

@@ -143,7 +143,9 @@ class RestoreLocalAttachmentJob private constructor(
return Result.success()
}
val streamSupplier = StreamSupplier { ArchiveFileSystem.openInputStream(context, restoreUri) ?: throw IOException("Unable to open stream") }
val streamSupplier = StreamSupplier {
ArchiveFileSystem.openInputStream(context, restoreUri) ?: throw IOException("Unable to open stream for $restoreUri")
}
try {
val iv = ByteArray(16)

View File

@@ -105,6 +105,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_NEW_LOCAL_BACKUPS_LAST_BACKUP_TIME = "backup.new_local_backups_last_backup_time"
private const val KEY_NEW_LOCAL_BACKUPS_SELECTED_SNAPSHOT_TIMESTAMP = "backup.new_local_backups_selected_snapshot_timestamp"
private const val KEY_LOCAL_RESTORE_ACCOUNT_ENTROPY_POOL = "backup.local_restore_account_entropy_pool"
private const val KEY_LOCAL_RESTORE_DIRECTORY_ERROR = "backup.local_restore_directory_error"
private const val KEY_UPLOAD_BANNER_VISIBLE = "backup.upload_banner_visible"
@@ -498,6 +499,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
* the account AEP because the local backup may belong to a different account (e.g., after ACI change).
*/
var localRestoreAccountEntropyPool: String? by stringValue(KEY_LOCAL_RESTORE_ACCOUNT_ENTROPY_POOL, null as String?)
var localRestoreDirectoryError: Boolean by booleanValue(KEY_LOCAL_RESTORE_DIRECTORY_ERROR, false)
/**
* When we are told by the server that we are out of storage space, we should show

View File

@@ -9806,5 +9806,14 @@
<!-- Label for internal-only section showing groups with same members -->
<string name="AddGroupDetailsFragment__groups_with_same_members" translatable="false">Groups with same members (Labs)</string>
<!-- Title of the sheet shown when a local backup restore could not be completed -->
<string name="CouldNotCompleteBackupRestoreSheet__title">Can\'t restore backup</string>
<!-- Body of the sheet shown when a local backup restore could not be completed, explaining the likely cause -->
<string name="CouldNotCompleteBackupRestoreSheet__body_error">An error occurred and your backup can\'t be restored. This may be because the backup folder was moved on your device while your backup was restoring.</string>
<!-- Body of the sheet shown when a local backup restore could not be completed, explaining how to try again -->
<string name="CouldNotCompleteBackupRestoreSheet__body_retry">To try again, uninstall and re-install Signal on this device, and choose \"Restore or transfer\".</string>
<!-- EOF -->
</resources>