mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-02 00:17:41 +01:00
Support directly selecting signalbackup.
This commit is contained in:
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.local
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
@@ -77,15 +78,28 @@ class ArchiveFileSystem private constructor(private val context: Context, root:
|
||||
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 openForRestore(context, root)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun openForRestore(context: Context, root: DocumentFile): ArchiveFileSystem? {
|
||||
if (root.findFile(MAIN_DIRECTORY_NAME) == null && !looksLikeSignalBackupsDirectory(root)) return null
|
||||
return try {
|
||||
ArchiveFileSystem(context, root, readOnly = true)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Unable to open backup directory for restore: $uri", e)
|
||||
Log.w(TAG, "Unable to open backup directory for restore", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if [dir] appears to be a SignalBackups directory based on its name and
|
||||
* expected internal structure (presence of the "files" subdirectory).
|
||||
*/
|
||||
private fun looksLikeSignalBackupsDirectory(dir: DocumentFile): Boolean {
|
||||
return dir.name == MAIN_DIRECTORY_NAME && dir.findFile("files") != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to create an [ArchiveFileSystem] from a regular [File].
|
||||
*
|
||||
@@ -105,12 +119,28 @@ class ArchiveFileSystem private constructor(private val context: Context, root:
|
||||
/** File access to shared super-set of archive related files (e.g., media + attachments) */
|
||||
val filesFileSystem: FilesFileSystem
|
||||
|
||||
/**
|
||||
* True if this file system was opened directly from the SignalBackups directory itself (rather than its parent).
|
||||
* In this case, the URI cannot be reused as a backup destination since we lack access to the parent directory.
|
||||
*/
|
||||
val isRootedAtSignalBackups: Boolean
|
||||
|
||||
init {
|
||||
if (readOnly) {
|
||||
signalBackups = root.findFile(MAIN_DIRECTORY_NAME) ?: throw IOException("SignalBackups directory not found in $root")
|
||||
val child = root.findFile(MAIN_DIRECTORY_NAME)
|
||||
if (child != null) {
|
||||
signalBackups = child
|
||||
isRootedAtSignalBackups = false
|
||||
} else if (looksLikeSignalBackupsDirectory(root)) {
|
||||
signalBackups = root
|
||||
isRootedAtSignalBackups = true
|
||||
} else {
|
||||
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 {
|
||||
isRootedAtSignalBackups = false
|
||||
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)
|
||||
|
||||
@@ -147,11 +147,13 @@ class RestoreLocalBackupActivityViewModel : ViewModel() {
|
||||
StorageServiceRestore.restore()
|
||||
RegistrationUtil.maybeMarkRegistrationComplete()
|
||||
|
||||
val canReenableBackups = backupIdMatchesCurrentAccount && !archiveFileSystem.isRootedAtSignalBackups
|
||||
|
||||
internalState.update {
|
||||
it.copy(
|
||||
restorePhase = RestorePhase.COMPLETE,
|
||||
backupDirectory = if (backupIdMatchesCurrentAccount) backupDirectory else null,
|
||||
dialog = if (backupIdMatchesCurrentAccount) RestoreLocalBackupActivityDialog.CONFIRM_BACKUP_LOCATION
|
||||
backupDirectory = if (canReenableBackups) backupDirectory else null,
|
||||
dialog = if (canReenableBackups) RestoreLocalBackupActivityDialog.CONFIRM_BACKUP_LOCATION
|
||||
else RestoreLocalBackupActivityDialog.LOCAL_BACKUPS_DISABLED
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.local
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNotNull
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class ArchiveFileSystemTest {
|
||||
|
||||
@get:Rule
|
||||
val temporaryFolder = TemporaryFolder()
|
||||
|
||||
private val context: Context = ApplicationProvider.getApplicationContext()
|
||||
|
||||
@Test
|
||||
fun `openForRestore succeeds when given the parent directory`() {
|
||||
val parent = temporaryFolder.newFolder()
|
||||
buildSignalBackupsStructure(parent)
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(parent))
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openForRestore isRootedAtSignalBackups is false when given the parent directory`() {
|
||||
val parent = temporaryFolder.newFolder()
|
||||
buildSignalBackupsStructure(parent)
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(parent))!!
|
||||
|
||||
assertThat(result.isRootedAtSignalBackups).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openForRestore succeeds when given the SignalBackups directory directly`() {
|
||||
val parent = temporaryFolder.newFolder()
|
||||
val signalBackups = buildSignalBackupsStructure(parent)
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(signalBackups))
|
||||
|
||||
assertThat(result).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openForRestore isRootedAtSignalBackups is true when given the SignalBackups directory directly`() {
|
||||
val parent = temporaryFolder.newFolder()
|
||||
val signalBackups = buildSignalBackupsStructure(parent)
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(signalBackups))!!
|
||||
|
||||
assertThat(result.isRootedAtSignalBackups).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openForRestore isRootedAtSignalBackups is false when parent is named SignalBackups but contains a real SignalBackups subfolder`() {
|
||||
val outerSignalBackups = temporaryFolder.newFolder(ArchiveFileSystem.MAIN_DIRECTORY_NAME)
|
||||
buildSignalBackupsStructure(outerSignalBackups)
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(outerSignalBackups))!!
|
||||
|
||||
assertThat(result.isRootedAtSignalBackups).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openForRestore returns null for a directory named SignalBackups without expected structure`() {
|
||||
val fakeSignalBackups = temporaryFolder.newFolder(ArchiveFileSystem.MAIN_DIRECTORY_NAME)
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(fakeSignalBackups))
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `openForRestore returns null for an unrelated directory`() {
|
||||
val unrelated = temporaryFolder.newFolder("SomeOtherFolder")
|
||||
|
||||
val result = ArchiveFileSystem.openForRestore(context, DocumentFile.fromFile(unrelated))
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the SignalBackups directory structure inside [parent] and returns the SignalBackups directory.
|
||||
*/
|
||||
private fun buildSignalBackupsStructure(parent: java.io.File): java.io.File {
|
||||
val signalBackups = parent.resolve(ArchiveFileSystem.MAIN_DIRECTORY_NAME).also { it.mkdir() }
|
||||
signalBackups.resolve("files").mkdir()
|
||||
return signalBackups
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user