mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 00:17:41 +01:00
Fix several bugs in the local backup restore flow.
This commit is contained in:
committed by
Cody Henthorne
parent
089d8a50b2
commit
9941b2d123
@@ -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)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<NavKey>() }
|
||||
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<RestoreLocalBackupNavKey.SelectLocalBackupTypeScreen> {
|
||||
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<RestoreLocalBackupNavKey.SelectLocalBackupTypeScreen> {
|
||||
SelectLocalBackupTypeScreen(
|
||||
onSelectBackupFolderClick = {
|
||||
backstack.add(RestoreLocalBackupNavKey.FolderInstructionSheet)
|
||||
},
|
||||
onSelectBackupFileClick = {
|
||||
backstack.add(RestoreLocalBackupNavKey.FileInstructionSheet)
|
||||
},
|
||||
onCancelClick = {
|
||||
backPressedDispatcher?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
entry<RestoreLocalBackupNavKey.FileInstructionSheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
SelectYourBackupFileSheetContent(onContinueClick = {
|
||||
fileLauncher.launch(OpenDocumentContract.Input())
|
||||
})
|
||||
}
|
||||
entry<RestoreLocalBackupNavKey.FileInstructionSheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
SelectYourBackupFileSheetContent(onContinueClick = {
|
||||
fileLauncher.launch(OpenDocumentContract.Input())
|
||||
})
|
||||
}
|
||||
|
||||
entry<RestoreLocalBackupNavKey.FolderInstructionSheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
SelectYourBackupFolderSheetContent(onContinueClick = {
|
||||
folderLauncher.launch(null)
|
||||
})
|
||||
}
|
||||
entry<RestoreLocalBackupNavKey.FolderInstructionSheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
SelectYourBackupFolderSheetContent(onContinueClick = {
|
||||
folderLauncher.launch(null)
|
||||
})
|
||||
}
|
||||
|
||||
entry<RestoreLocalBackupNavKey.SelectLocalBackupScreen> {
|
||||
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<RestoreLocalBackupNavKey.SelectLocalBackupScreen> {
|
||||
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<RestoreLocalBackupNavKey.SelectLocalBackupSheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
val dismissSheet = LocalBottomSheetDismiss.current
|
||||
SelectLocalBackupSheetContent(
|
||||
selectedBackup = requireNotNull(state.selectedBackup) { "No chosen backup." },
|
||||
selectableBackups = state.selectableBackups,
|
||||
onBackupSelected = {
|
||||
callback.setSelectedBackup(it)
|
||||
dismissSheet()
|
||||
}
|
||||
)
|
||||
}
|
||||
entry<RestoreLocalBackupNavKey.SelectLocalBackupSheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
val dismissSheet = LocalBottomSheetDismiss.current
|
||||
SelectLocalBackupSheetContent(
|
||||
selectedBackup = requireNotNull(state.selectedBackup) { "No chosen backup." },
|
||||
selectableBackups = state.selectableBackups,
|
||||
onBackupSelected = {
|
||||
callback.setSelectedBackup(it)
|
||||
dismissSheet()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
entry<RestoreLocalBackupNavKey.EnterLocalBackupKeyScreen> {
|
||||
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<RestoreLocalBackupNavKey.EnterLocalBackupKeyScreen> {
|
||||
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<RestoreLocalBackupNavKey.NoRecoveryKeySheet>(
|
||||
metadata = BottomSheetSceneStrategy.bottomSheet()
|
||||
) {
|
||||
val dismissSheet = LocalBottomSheetDismiss.current
|
||||
NoRecoveryKeySheetContent(
|
||||
onSkipAndDontRestoreClick = {
|
||||
dismissSheet()
|
||||
callback.displaySkipRestoreWarning()
|
||||
},
|
||||
onLearnMoreClick = {
|
||||
// TODO
|
||||
}
|
||||
)
|
||||
entry<RestoreLocalBackupNavKey.NoRecoveryKeySheet>(
|
||||
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<SelectableBackup> = persistentListOf()
|
||||
val selectableBackups: PersistentList<SelectableBackup> = 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -4937,6 +4937,8 @@
|
||||
<string name="BackupDialog_please_acknowledge_your_understanding_by_marking_the_confirmation_check_box">Please acknowledge your understanding by marking the confirmation check box.</string>
|
||||
<string name="BackupDialog_delete_backups">Delete backups?</string>
|
||||
<string name="BackupDialog_disable_and_delete_all_local_backups">Disable and delete all local backups?</string>
|
||||
<string name="ArchiveFileSystem__select_this_folder_hint_name">Select this folder, not SignalBackups</string>
|
||||
<string name="ArchiveFileSystem__select_this_folder_hint_body">When configuring Signal backup storage, select this folder — not the SignalBackups subfolder.</string>
|
||||
<string name="BackupDialog_delete_backups_statement">Delete backups</string>
|
||||
<!-- Progress dialog message shown while local backup files are being deleted -->
|
||||
<string name="BackupDialog_deleting_local_backup">Deleting local backup…</string>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user