From 9941b2d123ae723efbf8cc5f77ccf3717e403477 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 24 Mar 2026 15:39:09 -0300 Subject: [PATCH] Fix several bugs in the local backup restore flow. --- .../backup/v2/local/ArchiveFileSystem.kt | 63 ++++- .../ui/restore/EnterBackupKeyViewModel.kt | 2 +- .../RestoreLocalBackupActivityViewModel.kt | 2 +- .../local/RestoreLocalBackupFragment.kt | 2 +- .../local/RestoreLocalBackupNavDisplay.kt | 243 ++++++++++-------- .../local/RestoreLocalBackupViewModel.kt | 56 ++-- ...tRegistrationRestoreLocalBackupFragment.kt | 2 +- app/src/main/res/values/strings.xml | 2 + .../archive/stream/EncryptedBackupReader.kt | 3 +- 9 files changed, 221 insertions(+), 154 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt index 710686988d..a33483db31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt @@ -26,6 +26,7 @@ import org.signal.core.util.androidx.DocumentFileUtil.newFile import org.signal.core.util.androidx.DocumentFileUtil.outputStream import org.signal.core.util.androidx.DocumentFileUtil.renameTo import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.R import java.io.File import java.io.IOException import java.io.InputStream @@ -41,7 +42,7 @@ import java.util.concurrent.atomic.AtomicInteger * Provide a domain-specific interface to the root file system backing a local directory based archive. */ @Suppress("JoinDeclarationAndAssignment") -class ArchiveFileSystem private constructor(private val context: Context, root: DocumentFile) { +class ArchiveFileSystem private constructor(private val context: Context, root: DocumentFile, readOnly: Boolean = false) { companion object { val TAG = Log.tag(ArchiveFileSystem::class.java) @@ -51,7 +52,8 @@ class ArchiveFileSystem private constructor(private val context: Context, root: const val TEMP_BACKUP_DIRECTORY_SUFFIX: String = "tmp" /** - * Attempt to create an [ArchiveFileSystem] from a tree [Uri]. + * Attempt to create an [ArchiveFileSystem] from a tree [Uri], creating the necessary directory + * structure if it does not already exist. Use this when writing backups. * * Should likely only be called on API29+ */ @@ -62,7 +64,25 @@ class ArchiveFileSystem private constructor(private val context: Context, root: return null } - return ArchiveFileSystem(context, root) + return ArchiveFileSystem(context, root, readOnly = false) + } + + /** + * Attempt to open an existing [ArchiveFileSystem] from a tree [Uri] without creating any + * directories or files. Use this when reading/restoring backups. + * + * Should likely only be called on API29+ + */ + fun openForRestore(context: Context, uri: Uri): ArchiveFileSystem? { + val root = DocumentFile.fromTreeUri(context, uri) ?: return null + if (!root.canRead()) return null + if (root.findFile(MAIN_DIRECTORY_NAME) == null) return null + return try { + ArchiveFileSystem(context, root, readOnly = true) + } catch (e: IOException) { + Log.w(TAG, "Unable to open backup directory for restore: $uri", e) + null + } } /** @@ -71,7 +91,7 @@ class ArchiveFileSystem private constructor(private val context: Context, root: * Should likely only be called on < API29. */ fun fromFile(context: Context, backupDirectory: File): ArchiveFileSystem { - return ArchiveFileSystem(context, DocumentFile.fromFile(backupDirectory)) + return ArchiveFileSystem(context, DocumentFile.fromFile(backupDirectory), readOnly = false) } fun openInputStream(context: Context, uri: Uri): InputStream? { @@ -85,9 +105,22 @@ class ArchiveFileSystem private constructor(private val context: Context, root: val filesFileSystem: FilesFileSystem init { - signalBackups = root.mkdirp(MAIN_DIRECTORY_NAME) ?: throw IOException("Unable to create main backups directory") - val filesDirectory = signalBackups.mkdirp("files") ?: throw IOException("Unable to create files directory") - filesFileSystem = FilesFileSystem(context, filesDirectory) + if (readOnly) { + signalBackups = root.findFile(MAIN_DIRECTORY_NAME) ?: throw IOException("SignalBackups directory not found in $root") + val filesDirectory = signalBackups.findFile("files") ?: throw IOException("files directory not found in $signalBackups") + filesFileSystem = FilesFileSystem(context, filesDirectory, readOnly = true) + } else { + signalBackups = root.mkdirp(MAIN_DIRECTORY_NAME) ?: throw IOException("Unable to create main backups directory") + val filesDirectory = signalBackups.mkdirp("files") ?: throw IOException("Unable to create files directory") + filesFileSystem = FilesFileSystem(context, filesDirectory) + + val hintFileName = context.getString(R.string.ArchiveFileSystem__select_this_folder_hint_name) + if (!root.hasFile(hintFileName)) { + root.createFile("text/plain", hintFileName) + ?.outputStream(context) + ?.use { out -> out.write(context.getString(R.string.ArchiveFileSystem__select_this_folder_hint_body).toByteArray()) } + } + } } /** @@ -258,7 +291,7 @@ class SnapshotFileSystem(private val context: Context, private val snapshotDirec /** * Domain specific file system access for accessing backup files (e.g., attachments, media, etc.). */ -class FilesFileSystem(private val context: Context, private val root: DocumentFile) { +class FilesFileSystem(private val context: Context, private val root: DocumentFile, readOnly: Boolean = false) { companion object { private val TAG = Log.tag(FilesFileSystem::class.java) @@ -271,11 +304,15 @@ class FilesFileSystem(private val context: Context, private val root: DocumentFi .mapNotNull { f -> f.name?.let { name -> name to f } } .toMap() - subFolders = (0..255) - .map { i -> i.toString(16).padStart(2, '0') } - .associateWith { name -> - existingFolders[name] ?: root.createDirectory(name)!! - } + subFolders = if (readOnly) { + existingFolders + } else { + (0..255) + .map { i -> i.toString(16).padStart(2, '0') } + .associateWith { name -> + existingFolders[name] ?: root.createDirectory(name)!! + } + } } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyViewModel.kt index 61c374df6f..bcea6a226e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/EnterBackupKeyViewModel.kt @@ -86,7 +86,7 @@ class EnterBackupKeyViewModel : ViewModel() { val aep = AccountEntropyPool.parseOrNull(backupKey) ?: return false val dirUri = SignalStore.backup.newLocalBackupsDirectory ?: return false - val archiveFileSystem = ArchiveFileSystem.fromUri(AppDependencies.application, Uri.parse(dirUri)) ?: return false + val archiveFileSystem = ArchiveFileSystem.openForRestore(AppDependencies.application, Uri.parse(dirUri)) ?: return false val snapshot = archiveFileSystem.listSnapshots().firstOrNull { it.timestamp == selectedTimestamp } ?: return false val snapshotFs = SnapshotFileSystem(AppDependencies.application, snapshot.file) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt index e3fcff3df9..ac16b636c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupActivityViewModel.kt @@ -93,7 +93,7 @@ class RestoreLocalBackupActivityViewModel : ViewModel() { return@launch } - val archiveFileSystem = ArchiveFileSystem.fromUri(AppDependencies.application, Uri.parse(backupDirectory)) + val archiveFileSystem = ArchiveFileSystem.openForRestore(AppDependencies.application, Uri.parse(backupDirectory)) if (archiveFileSystem == null) { Log.w(TAG, "Unable to access backup directory: $backupDirectory") internalState.update { it.copy(restorePhase = RestorePhase.FAILED) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt index 8ab0de4205..2a67f0ed7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupFragment.kt @@ -118,7 +118,7 @@ class RestoreLocalBackupFragment : ComposeFragment() { restoreLocalBackupViewModel.setSelectedBackup(backup) } - override fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean { + override suspend fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean { return restoreLocalBackupViewModel.setSelectedBackupDirectory(context, uri) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt index 1e176e79d8..1625336455 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupNavDisplay.kt @@ -9,9 +9,15 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider @@ -19,6 +25,7 @@ import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.launch import org.signal.core.ui.compose.Launchers import org.signal.core.ui.contracts.OpenDocumentContract import org.signal.core.ui.navigation.BottomSheetSceneStrategy @@ -41,14 +48,17 @@ fun RestoreLocalBackupNavDisplay( val bottomSheetStrategy = remember { BottomSheetSceneStrategy() } val backPressedDispatcher = LocalOnBackPressedDispatcherOwner.current val context = LocalContext.current + val scope = rememberCoroutineScope() val folderLauncher = Launchers.rememberOpenDocumentTreeLauncher { if (it != null) { val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION context.contentResolver.takePersistableUriPermission(it, takeFlags) - if (callback.setSelectedBackupDirectory(context, it)) { - backstack.add(RestoreLocalBackupNavKey.SelectLocalBackupScreen) + scope.launch { + if (callback.setSelectedBackupDirectory(context, it)) { + backstack.add(RestoreLocalBackupNavKey.SelectLocalBackupScreen) + } } } } @@ -59,133 +69,140 @@ fun RestoreLocalBackupNavDisplay( } } - NavDisplay( - backStack = backstack, - sceneStrategy = bottomSheetStrategy, - entryProvider = entryProvider { - entry { - SelectLocalBackupTypeScreen( - onSelectBackupFolderClick = { - backstack.add(RestoreLocalBackupNavKey.FolderInstructionSheet) - }, - onSelectBackupFileClick = { - backstack.add(RestoreLocalBackupNavKey.FileInstructionSheet) - }, - onCancelClick = { - backPressedDispatcher?.onBackPressedDispatcher?.onBackPressed() - } - ) - } + Box(modifier = Modifier.fillMaxSize()) { + NavDisplay( + backStack = backstack, + sceneStrategy = bottomSheetStrategy, + entryProvider = entryProvider { + entry { + SelectLocalBackupTypeScreen( + onSelectBackupFolderClick = { + backstack.add(RestoreLocalBackupNavKey.FolderInstructionSheet) + }, + onSelectBackupFileClick = { + backstack.add(RestoreLocalBackupNavKey.FileInstructionSheet) + }, + onCancelClick = { + backPressedDispatcher?.onBackPressedDispatcher?.onBackPressed() + } + ) + } - entry( - metadata = BottomSheetSceneStrategy.bottomSheet() - ) { - SelectYourBackupFileSheetContent(onContinueClick = { - fileLauncher.launch(OpenDocumentContract.Input()) - }) - } + entry( + metadata = BottomSheetSceneStrategy.bottomSheet() + ) { + SelectYourBackupFileSheetContent(onContinueClick = { + fileLauncher.launch(OpenDocumentContract.Input()) + }) + } - entry( - metadata = BottomSheetSceneStrategy.bottomSheet() - ) { - SelectYourBackupFolderSheetContent(onContinueClick = { - folderLauncher.launch(null) - }) - } + entry( + metadata = BottomSheetSceneStrategy.bottomSheet() + ) { + SelectYourBackupFolderSheetContent(onContinueClick = { + folderLauncher.launch(null) + }) + } - entry { - SelectLocalBackupScreen( - selectedBackup = requireNotNull(state.selectedBackup) { "No chosen backup." }, - isSelectedBackupLatest = state.selectedBackup == state.selectableBackups.firstOrNull(), - onRestoreBackupClick = { - backstack.add(RestoreLocalBackupNavKey.EnterLocalBackupKeyScreen) - }, - onCancelClick = { - backPressedDispatcher?.onBackPressedDispatcher?.onBackPressed() - }, - onChooseADifferentBackupClick = { - backstack.add(RestoreLocalBackupNavKey.SelectLocalBackupSheet) - } - ) - } + entry { + SelectLocalBackupScreen( + selectedBackup = requireNotNull(state.selectedBackup) { "No chosen backup." }, + isSelectedBackupLatest = state.selectedBackup == state.selectableBackups.firstOrNull(), + onRestoreBackupClick = { + backstack.add(RestoreLocalBackupNavKey.EnterLocalBackupKeyScreen) + }, + onCancelClick = { + backPressedDispatcher?.onBackPressedDispatcher?.onBackPressed() + }, + onChooseADifferentBackupClick = { + backstack.add(RestoreLocalBackupNavKey.SelectLocalBackupSheet) + } + ) + } - entry( - metadata = BottomSheetSceneStrategy.bottomSheet() - ) { - val dismissSheet = LocalBottomSheetDismiss.current - SelectLocalBackupSheetContent( - selectedBackup = requireNotNull(state.selectedBackup) { "No chosen backup." }, - selectableBackups = state.selectableBackups, - onBackupSelected = { - callback.setSelectedBackup(it) - dismissSheet() - } - ) - } + entry( + metadata = BottomSheetSceneStrategy.bottomSheet() + ) { + val dismissSheet = LocalBottomSheetDismiss.current + SelectLocalBackupSheetContent( + selectedBackup = requireNotNull(state.selectedBackup) { "No chosen backup." }, + selectableBackups = state.selectableBackups, + onBackupSelected = { + callback.setSelectedBackup(it) + dismissSheet() + } + ) + } - entry { - EnterLocalBackupKeyScreen( - backupKey = backupKey, - isRegistrationInProgress = isRegistrationInProgress, - isBackupKeyValid = enterBackupKeyState.backupKeyValid, - aepValidationError = enterBackupKeyState.aepValidationError, - onBackupKeyChanged = callback::onBackupKeyChanged, - onNextClicked = callback::submitBackupKey, - onNoBackupKeyClick = { - backstack.add(RestoreLocalBackupNavKey.NoRecoveryKeySheet) - }, - showRegistrationError = enterBackupKeyState.showRegistrationError, - registerAccountResult = enterBackupKeyState.registerAccountResult, - onRegistrationErrorDismiss = callback::clearRegistrationError, - onBackupKeyHelp = callback::onBackupKeyHelp - ) - } + entry { + EnterLocalBackupKeyScreen( + backupKey = backupKey, + isRegistrationInProgress = isRegistrationInProgress, + isBackupKeyValid = enterBackupKeyState.backupKeyValid, + aepValidationError = enterBackupKeyState.aepValidationError, + onBackupKeyChanged = callback::onBackupKeyChanged, + onNextClicked = callback::submitBackupKey, + onNoBackupKeyClick = { + backstack.add(RestoreLocalBackupNavKey.NoRecoveryKeySheet) + }, + showRegistrationError = enterBackupKeyState.showRegistrationError, + registerAccountResult = enterBackupKeyState.registerAccountResult, + onRegistrationErrorDismiss = callback::clearRegistrationError, + onBackupKeyHelp = callback::onBackupKeyHelp + ) + } - entry( - metadata = BottomSheetSceneStrategy.bottomSheet() - ) { - val dismissSheet = LocalBottomSheetDismiss.current - NoRecoveryKeySheetContent( - onSkipAndDontRestoreClick = { - dismissSheet() - callback.displaySkipRestoreWarning() - }, - onLearnMoreClick = { - // TODO - } - ) + entry( + metadata = BottomSheetSceneStrategy.bottomSheet() + ) { + val dismissSheet = LocalBottomSheetDismiss.current + NoRecoveryKeySheetContent( + onSkipAndDontRestoreClick = { + dismissSheet() + callback.displaySkipRestoreWarning() + }, + onLearnMoreClick = { + // TODO + } + ) + } } + ) + + RestoreLocalBackupDialogDisplay( + dialog = state.dialog, + onDialogConfirmed = { + when (it) { + RestoreLocalBackupDialog.SKIP_RESTORE_WARNING -> callback.skipRestore() + RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> callback.confirmRestoreWithDifferentAccount() + else -> Unit + } + }, + onDialogDenied = { + when (it) { + RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> callback.denyRestoreWithDifferentAccount() + else -> Unit + } + }, + onDismiss = callback::clearDialog + ) + + if (state.isLoadingBackupDirectory) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } - ) - - RestoreLocalBackupDialogDisplay( - dialog = state.dialog, - onDialogConfirmed = { - when (it) { - RestoreLocalBackupDialog.SKIP_RESTORE_WARNING -> callback.skipRestore() - RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> callback.confirmRestoreWithDifferentAccount() - else -> Unit - } - }, - onDialogDenied = { - when (it) { - RestoreLocalBackupDialog.CONFIRM_DIFFERENT_ACCOUNT -> callback.denyRestoreWithDifferentAccount() - else -> Unit - } - }, - onDismiss = callback::clearDialog - ) + } } data class RestoreLocalBackupState( val dialog: RestoreLocalBackupDialog? = null, val selectedBackup: SelectableBackup? = null, - val selectableBackups: PersistentList = persistentListOf() + val selectableBackups: PersistentList = persistentListOf(), + val isLoadingBackupDirectory: Boolean = false ) interface RestoreLocalBackupCallback { fun setSelectedBackup(backup: SelectableBackup) - fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean + suspend fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean fun displaySkipRestoreWarning() fun clearDialog() fun skipRestore() @@ -199,7 +216,7 @@ interface RestoreLocalBackupCallback { object Empty : RestoreLocalBackupCallback { override fun setSelectedBackup(backup: SelectableBackup) = Unit - override fun setSelectedBackupDirectory(context: Context, uri: Uri) = false + override suspend fun setSelectedBackupDirectory(context: Context, uri: Uri) = false override fun displaySkipRestoreWarning() = Unit override fun clearDialog() = Unit override fun skipRestore() = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt index 6066500c93..458598b51d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/restore/local/RestoreLocalBackupViewModel.kt @@ -40,41 +40,51 @@ class RestoreLocalBackupViewModel : ViewModel() { internalState.update { it.copy(selectedBackup = backup) } } - fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean { + suspend fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean { SignalStore.backup.newLocalBackupsDirectory = uri.toString() + internalState.update { it.copy(isLoadingBackupDirectory = true) } - val archiveFileSystem = ArchiveFileSystem.fromUri(context, uri) + val archiveFileSystem = withContext(Dispatchers.IO) { ArchiveFileSystem.openForRestore(context, uri) } if (archiveFileSystem == null) { Log.w(TAG, "Unable to access backup directory: $uri") - internalState.update { it.copy(selectedBackup = null, selectableBackups = persistentListOf(), dialog = RestoreLocalBackupDialog.FAILED_TO_LOAD_ARCHIVE) } + internalState.update { it.copy(isLoadingBackupDirectory = false, selectedBackup = null, selectableBackups = persistentListOf(), dialog = RestoreLocalBackupDialog.FAILED_TO_LOAD_ARCHIVE) } return false } - val selectableBackups = archiveFileSystem - .listSnapshots() - .take(2) - .map { snapshot -> - val dateLabel = if (DateUtils.isSameDay(System.currentTimeMillis(), snapshot.timestamp)) { - context.getString(R.string.DateUtils_today) - } else { - DateUtils.formatDateWithYear(Locale.getDefault(), snapshot.timestamp) - } - val timeLabel = DateUtils.getOnlyTimeString(context, snapshot.timestamp) - val sizeBytes = SnapshotFileSystem(context, snapshot.file).mainLength() ?: 0L + val selectableBackups = withContext(Dispatchers.IO) { + archiveFileSystem + .listSnapshots() + .take(2) + .map { snapshot -> + val dateLabel = if (DateUtils.isSameDay(System.currentTimeMillis(), snapshot.timestamp)) { + context.getString(R.string.DateUtils_today) + } else { + DateUtils.formatDateWithYear(Locale.getDefault(), snapshot.timestamp) + } + val timeLabel = DateUtils.getOnlyTimeString(context, snapshot.timestamp) + val sizeBytes = SnapshotFileSystem(context, snapshot.file).mainLength() ?: 0L - SelectableBackup( - timestamp = snapshot.timestamp, - backupTime = "$dateLabel • $timeLabel", - backupSize = sizeBytes.bytes.toUnitString() - ) - } - .toPersistentList() + SelectableBackup( + timestamp = snapshot.timestamp, + backupTime = "$dateLabel • $timeLabel", + backupSize = sizeBytes.bytes.toUnitString() + ) + } + .toPersistentList() + } + + if (selectableBackups.isEmpty()) { + Log.w(TAG, "No snapshots found in backup directory: $uri") + internalState.update { it.copy(isLoadingBackupDirectory = false, selectedBackup = null, selectableBackups = persistentListOf(), dialog = RestoreLocalBackupDialog.FAILED_TO_LOAD_ARCHIVE) } + return false + } internalState.update { it.copy( + isLoadingBackupDirectory = false, selectableBackups = selectableBackups, - selectedBackup = selectableBackups.firstOrNull() + selectedBackup = selectableBackups.first() ) } return true @@ -94,7 +104,7 @@ class RestoreLocalBackupViewModel : ViewModel() { val aep = requireNotNull(AccountEntropyPool.parseOrNull(backupKey)) { "Backup key must be valid at submission time" } val messageBackupKey = aep.deriveMessageBackupKey() val dirUri = requireNotNull(SignalStore.backup.newLocalBackupsDirectory) { "Backup directory must be set" } - val archiveFileSystem = requireNotNull(ArchiveFileSystem.fromUri(context, Uri.parse(dirUri))) { "Backup directory must be accessible" } + val archiveFileSystem = requireNotNull(ArchiveFileSystem.openForRestore(context, Uri.parse(dirUri))) { "Backup directory must be accessible" } val snapshot = requireNotNull(archiveFileSystem.listSnapshots().firstOrNull { it.timestamp == timestamp }) { "Selected snapshot must still exist" } val snapshotFs = SnapshotFileSystem(context, snapshot.file) val actualBackupId = LocalArchiver.getBackupId(snapshotFs, messageBackupKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt index 36d3ba5d27..610ac9efa8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/restore/local/PostRegistrationRestoreLocalBackupFragment.kt @@ -74,7 +74,7 @@ class PostRegistrationRestoreLocalBackupFragment : ComposeFragment() { restoreLocalBackupViewModel.setSelectedBackup(backup) } - override fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean { + override suspend fun setSelectedBackupDirectory(context: Context, uri: Uri): Boolean { return restoreLocalBackupViewModel.setSelectedBackupDirectory(context, uri) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb090027eb..cb136efb83 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4937,6 +4937,8 @@ Please acknowledge your understanding by marking the confirmation check box. Delete backups? Disable and delete all local backups? + Select this folder, not SignalBackups + When configuring Signal backup storage, select this folder — not the SignalBackups subfolder. Delete backups Deleting local backup… diff --git a/lib/archive/src/main/java/org/signal/archive/stream/EncryptedBackupReader.kt b/lib/archive/src/main/java/org/signal/archive/stream/EncryptedBackupReader.kt index 396e99959f..3834cf6c97 100644 --- a/lib/archive/src/main/java/org/signal/archive/stream/EncryptedBackupReader.kt +++ b/lib/archive/src/main/java/org/signal/archive/stream/EncryptedBackupReader.kt @@ -227,10 +227,11 @@ class EncryptedBackupReader private constructor( try { val length = stream.readVarInt32().also { if (it < 0) return null } val frameBytes: ByteArray = stream.readNBytesOrThrow(length) - return Frame.ADAPTER.decode(frameBytes) } catch (e: EOFException) { return null + } catch (e: IOException) { + return read() } }