diff --git a/README.md b/README.md
index 9ffb0fae..41ccd2e9 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need
* `android.permission.FOREGROUND_SERVICE` to do periodic storage backups without interruption.
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots (optional) for better UX.
* `android.permission.USE_BIOMETRIC` to authenticate saving a new recovery code
+* `android.permission.INTERACT_ACROSS_USERS_FULL` to use storage roots in other users (optional).
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/seedvault-app/seedvault.
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a69d8664..8fcb13f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -60,6 +60,11 @@
android:name="com.stevesoltys.seedvault.RESTORE_BACKUP"
android:protectionLevel="system|signature" />
+
+
+
permitDiskReads(func: () -> T): T {
func()
}
}
+
+fun Context.getSystemContext(isUsbStorage: () -> Boolean): Context {
+ return if (checkSelfPermission(INTERACT_ACROSS_USERS_FULL) == PERMISSION_GRANTED
+ && isUsbStorage()) createContextAsUser(UserHandle.SYSTEM, 0) else this
+}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt
index 3eaebc73..04a3cb00 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsProviderStoragePlugin.kt
@@ -5,6 +5,7 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
+import com.stevesoltys.seedvault.getSystemContext
import com.stevesoltys.seedvault.plugins.EncryptedMetadata
import com.stevesoltys.seedvault.plugins.StoragePlugin
import java.io.FileNotFoundException
@@ -16,11 +17,15 @@ private val TAG = DocumentsProviderStoragePlugin::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderStoragePlugin(
- private val context: Context,
+ private val appContext: Context,
private val storage: DocumentsStorage,
) : StoragePlugin {
- private val packageManager: PackageManager = context.packageManager
+ private val context: Context get() = appContext.getSystemContext {
+ storage.storage?.isUsb == true
+ }
+
+ private val packageManager: PackageManager = appContext.packageManager
@Throws(IOException::class)
override suspend fun startNewRestoreSet(token: Long) {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt
index cfd53f41..a174f29d 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/plugins/saf/DocumentsStorage.kt
@@ -2,6 +2,7 @@
package com.stevesoltys.seedvault.plugins.saf
+import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageInfo
import android.database.ContentObserver
@@ -15,6 +16,7 @@ import android.provider.DocumentsContract.getDocumentId
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.documentfile.provider.DocumentFile
+import com.stevesoltys.seedvault.getSystemContext
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.Storage
import kotlinx.coroutines.TimeoutCancellationException
@@ -40,11 +42,15 @@ const val MIME_TYPE = "application/octet-stream"
private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage(
- private val context: Context,
+ private val appContext: Context,
private val settingsManager: SettingsManager,
) {
- private val contentResolver = context.contentResolver
+ private val context: Context get() = appContext.getSystemContext {
+ storage?.isUsb ?: false
+ }
+
+ private val contentResolver: ContentResolver get() = context.contentResolver
internal var storage: Storage? = null
get() {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
index 2158d018..18179eed 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsManager.kt
@@ -5,6 +5,7 @@ import android.hardware.usb.UsbDevice
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
+import android.os.UserHandle
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
@@ -121,7 +122,8 @@ class SettingsManager(private val context: Context) {
@WorkerThread
fun canDoBackupNow(): Boolean {
val storage = getStorage() ?: return false
- return !storage.isUnavailableUsb(context) && !storage.isUnavailableNetwork(context)
+ return !storage.isUnavailableUsb(context.createContextAsUser(UserHandle.SYSTEM, 0))
+ && !storage.isUnavailableNetwork(context)
}
fun backupApks(): Boolean {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt
index 9908061a..36dd62dc 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/storage/SeedvaultStoragePlugin.kt
@@ -3,15 +3,20 @@ package com.stevesoltys.seedvault.storage
import android.content.Context
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.crypto.KeyManager
+import com.stevesoltys.seedvault.getSystemContext
import com.stevesoltys.seedvault.plugins.saf.DocumentsStorage
import org.calyxos.backup.storage.plugin.saf.SafStoragePlugin
import javax.crypto.SecretKey
internal class SeedvaultStoragePlugin(
- context: Context,
+ private val appContext: Context,
private val storage: DocumentsStorage,
private val keyManager: KeyManager,
-) : SafStoragePlugin(context) {
+) : SafStoragePlugin(appContext) {
+ override val context: Context
+ get() = appContext.getSystemContext {
+ storage.storage?.isUsb == true
+ }
override val root: DocumentFile
get() = storage.rootBackupDir ?: error("No storage set")
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
index 6bd88464..011e1e10 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/RestoreStorageViewModel.kt
@@ -32,7 +32,6 @@ internal class RestoreStorageViewModel(
}
if (hasBackup) {
saveStorage(uri)
-
mLocationChecked.postEvent(LocationResult())
} else {
Log.w(TAG, "Location was rejected: $uri")
diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt
index ead92a8f..fcbeaa0f 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/ui/storage/StorageRootResolver.kt
@@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.ui.storage
import android.content.Context
import android.database.Cursor
import android.graphics.drawable.Drawable
+import android.os.UserHandle
import android.provider.DocumentsContract
import android.provider.DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
import android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID
@@ -23,6 +24,8 @@ internal object StorageRootResolver {
private val TAG = StorageRootResolver::class.java.simpleName
+ private const val usbAuthority = "com.android.externalstorage.documents"
+
fun getStorageRoots(context: Context, authority: String): List {
val roots = ArrayList()
val rootsUri = DocumentsContract.buildRootsUri(authority)
@@ -34,6 +37,16 @@ internal object StorageRootResolver {
if (root != null) roots.add(root)
}
}
+ if (usbAuthority == authority && UserHandle.myUserId() != UserHandle.USER_SYSTEM) {
+ val c: Context = context.createContextAsUser(UserHandle.SYSTEM, 0)
+ c.contentResolver.query(rootsUri, null, null, null, null)?.use { cursor ->
+ while (cursor.moveToNext()) {
+ // Pass in context since it is used to query package manager for app icons
+ val root = getStorageRoot(context, authority, cursor)
+ if (root != null && root.isUsb) roots.add(root)
+ }
+ }
+ }
} catch (e: Exception) {
Log.w(TAG, "Failed to load some roots from $authority", e)
}
diff --git a/permissions_com.stevesoltys.seedvault.xml b/permissions_com.stevesoltys.seedvault.xml
index 640c0811..d7bf61ed 100644
--- a/permissions_com.stevesoltys.seedvault.xml
+++ b/permissions_com.stevesoltys.seedvault.xml
@@ -4,6 +4,7 @@
+
diff --git a/storage/demo/src/main/java/de/grobox/storagebackuptester/plugin/TestSafStoragePlugin.kt b/storage/demo/src/main/java/de/grobox/storagebackuptester/plugin/TestSafStoragePlugin.kt
index 286f13d5..9ee75bc7 100644
--- a/storage/demo/src/main/java/de/grobox/storagebackuptester/plugin/TestSafStoragePlugin.kt
+++ b/storage/demo/src/main/java/de/grobox/storagebackuptester/plugin/TestSafStoragePlugin.kt
@@ -11,10 +11,11 @@ import javax.crypto.SecretKey
@Suppress("BlockingMethodInNonBlockingContext")
class TestSafStoragePlugin(
- private val context: Context,
+ private val appContext: Context,
private val getLocationUri: () -> Uri?,
-) : SafStoragePlugin(context) {
+) : SafStoragePlugin(appContext) {
+ override val context = appContext
override val root: DocumentFile?
get() {
val uri = getLocationUri() ?: return null
diff --git a/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/saf/SafStoragePlugin.kt b/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/saf/SafStoragePlugin.kt
index 249c733c..9fb988ac 100644
--- a/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/saf/SafStoragePlugin.kt
+++ b/storage/lib/src/main/java/org/calyxos/backup/storage/plugin/saf/SafStoragePlugin.kt
@@ -28,12 +28,18 @@ internal const val CHUNK_FOLDER_COUNT = 256
private const val TAG = "SafStoragePlugin"
+/**
+ * @param appContext application context provided by the storage module
+ */
@Suppress("BlockingMethodInNonBlockingContext")
public abstract class SafStoragePlugin(
- private val context: Context,
+ private val appContext: Context,
) : StoragePlugin {
private val cache = SafCache()
+ // In the case of USB storage, if INTERACT_ACROSS_USERS_FULL is granted, this context will match
+ // the system user's application context. Otherwise, matches appContext.
+ protected abstract val context: Context
protected abstract val root: DocumentFile?
private val folder: DocumentFile?
@@ -44,7 +50,7 @@ public abstract class SafStoragePlugin(
@SuppressLint("HardwareIds")
// this is unique to each combination of app-signing key, user, and device
// so we don't leak anything by not hashing this and can use it as is
- val androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID)
+ val androidId = Settings.Secure.getString(appContext.contentResolver, ANDROID_ID)
// the folder name is our user ID
val folderName = "$androidId.sv"
cache.currentFolder = try {
@@ -56,8 +62,6 @@ public abstract class SafStoragePlugin(
return cache.currentFolder
}
- private val contentResolver = context.contentResolver
-
private fun timestampToSnapshot(timestamp: Long): String {
return "$timestamp.SeedSnap"
}
@@ -153,7 +157,7 @@ public abstract class SafStoragePlugin(
val name = timestampToSnapshot(timestamp)
// TODO should we check if it exists first?
val snapshotFile = folder.createFileOrThrow(name, MIME_TYPE)
- return snapshotFile.getOutputStream(contentResolver)
+ return snapshotFile.getOutputStream(context.contentResolver)
}
/************************* Restore *******************************/
@@ -188,7 +192,7 @@ public abstract class SafStoragePlugin(
val snapshotFile = cache.snapshotFiles.getOrElse(storedSnapshot) {
getFolder(storedSnapshot).findFileBlocking(context, timestampToSnapshot(timestamp))
} ?: throw IOException("Could not get file for snapshot $timestamp")
- return snapshotFile.getInputStream(contentResolver)
+ return snapshotFile.getInputStream(context.contentResolver)
}
@Throws(IOException::class)