Add support for d2d
Co-authored-by: Tommy Webb <tommy@calyxinstitute.org> Change-Id: I61d88a511a0f81e6d384e3650f6797725da79814
This commit is contained in:
parent
befc1b1a77
commit
f0575ddd6a
10 changed files with 123 additions and 50 deletions
|
|
@ -5,7 +5,10 @@ import android.hardware.usb.UsbDevice
|
|||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.VisibleForTesting.PRIVATE
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
|
|
@ -34,6 +37,8 @@ private const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
|
|||
|
||||
class SettingsManager(private val context: Context) {
|
||||
|
||||
private val TAG = SettingsManager::class.java.simpleName
|
||||
|
||||
private val prefs = permitDiskReads {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
|
@ -41,12 +46,19 @@ class SettingsManager(private val context: Context) {
|
|||
@Volatile
|
||||
private var token: Long? = null
|
||||
|
||||
/**
|
||||
* The StoragePlugin provider package name is assigned by ConfigurableBackupTransportService
|
||||
* to ensure it is excluded; if the provider is killed, the backup or restore will fail.
|
||||
*/
|
||||
var pluginProviderPackageName: String? = null
|
||||
|
||||
/**
|
||||
* This gets accessed by non-UI threads when saving with [PreferenceManager]
|
||||
* and when [isBackupEnabled] is called during a backup run.
|
||||
* Therefore, it is implemented with a thread-safe [ConcurrentSkipListSet].
|
||||
*/
|
||||
private val blacklistedApps: MutableSet<String> by lazy {
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
val blacklistedApps: MutableSet<String> by lazy {
|
||||
ConcurrentSkipListSet(prefs.getStringSet(PREF_KEY_BACKUP_APP_BLACKLIST, emptySet()))
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +144,15 @@ class SettingsManager(private val context: Context) {
|
|||
|
||||
fun isBackupEnabled(packageName: String) = !blacklistedApps.contains(packageName)
|
||||
|
||||
fun isAppAllowedForBackup(packageName: String): Boolean {
|
||||
// Check that the app is not blacklisted by the user
|
||||
val enabled = isBackupEnabled(packageName)
|
||||
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.")
|
||||
// We need to exclude the DocumentsProvider used to store backup data.
|
||||
// Otherwise, it gets killed when we back it up, terminating our backup.
|
||||
return enabled && packageName != pluginProviderPackageName
|
||||
}
|
||||
|
||||
fun isStorageBackupEnabled() = prefs.getBoolean(PREF_KEY_BACKUP_STORAGE, false)
|
||||
|
||||
@UiThread
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import android.os.ParcelFileDescriptor
|
|||
import android.util.Log
|
||||
import com.stevesoltys.seedvault.R
|
||||
import com.stevesoltys.seedvault.settings.SettingsActivity
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.backup.BackupCoordinator
|
||||
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
|
@ -22,7 +23,7 @@ import org.koin.core.component.inject
|
|||
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
|
||||
|
||||
// Since there seems to be consensus in the community to pose as device to device transport,
|
||||
// we are pretending to be one here. This will backup opt-out apps that target API 30.
|
||||
// we are pretending to be one here. This will backup opt-out apps that target at least API 30.
|
||||
const val TRANSPORT_FLAGS = FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED or FLAG_DEVICE_TO_DEVICE_TRANSFER
|
||||
|
||||
private const val TRANSPORT_DIRECTORY_NAME =
|
||||
|
|
@ -38,6 +39,7 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
|||
|
||||
private val backupCoordinator by inject<BackupCoordinator>()
|
||||
private val restoreCoordinator by inject<RestoreCoordinator>()
|
||||
private val settingsManager by inject<SettingsManager>()
|
||||
|
||||
override fun transportDirName(): String {
|
||||
return TRANSPORT_DIRECTORY_NAME
|
||||
|
|
@ -120,9 +122,9 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
|
|||
|
||||
override fun isAppEligibleForBackup(
|
||||
targetPackage: PackageInfo,
|
||||
isFullBackup: Boolean,
|
||||
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
||||
): Boolean {
|
||||
return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
|
||||
return settingsManager.isAppAllowedForBackup(targetPackage.packageName)
|
||||
}
|
||||
|
||||
override fun getBackupQuota(packageName: String, isFullBackup: Boolean): Long = runBlocking {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import android.util.Log
|
|||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.BackupMonitor
|
||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
|
||||
|
|
@ -65,7 +67,13 @@ fun requestBackup(context: Context) {
|
|||
val backupManager: IBackupManager = get().get()
|
||||
if (backupManager.isBackupEnabled) {
|
||||
val packageService: PackageService = get().get()
|
||||
val packages = packageService.eligiblePackages
|
||||
val settingsManager: SettingsManager = get().get()
|
||||
val plugin: StoragePlugin = get().get()
|
||||
|
||||
// SettingsManager must know the StoragePlugin provider package name to exclude it when
|
||||
// isAppAllowedForBackup is called.
|
||||
settingsManager.pluginProviderPackageName = plugin.providerPackageName
|
||||
val packages = packageService.requestedPackages
|
||||
val appTotals = packageService.expectedAppTotals
|
||||
|
||||
val result = try {
|
||||
|
|
|
|||
|
|
@ -138,19 +138,6 @@ internal class BackupCoordinator(
|
|||
TRANSPORT_ERROR
|
||||
}
|
||||
|
||||
fun isAppEligibleForBackup(
|
||||
targetPackage: PackageInfo,
|
||||
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
||||
): Boolean {
|
||||
val packageName = targetPackage.packageName
|
||||
// Check that the app is not blacklisted by the user
|
||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.")
|
||||
// We need to exclude the DocumentsProvider used to store backup data.
|
||||
// Otherwise, it gets killed when we back it up, terminating our backup.
|
||||
return enabled && targetPackage.packageName != plugin.providerPackageName
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the transport about current quota for backup size of the package.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ val backupModule = module {
|
|||
single {
|
||||
PackageService(
|
||||
context = androidContext(),
|
||||
backupManager = get()
|
||||
settingsManager = get(),
|
||||
)
|
||||
}
|
||||
single {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.IBackupManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
||||
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
||||
|
|
@ -17,6 +16,7 @@ import android.util.Log
|
|||
import android.util.Log.INFO
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
|
||||
private val TAG = PackageService::class.java.simpleName
|
||||
|
||||
|
|
@ -28,13 +28,13 @@ private const val LOG_MAX_PACKAGES = 100
|
|||
*/
|
||||
internal class PackageService(
|
||||
private val context: Context,
|
||||
private val backupManager: IBackupManager,
|
||||
private val settingsManager: SettingsManager,
|
||||
) {
|
||||
|
||||
private val packageManager: PackageManager = context.packageManager
|
||||
private val myUserId = UserHandle.myUserId()
|
||||
|
||||
val eligiblePackages: Array<String>
|
||||
val requestedPackages: Array<String>
|
||||
@WorkerThread
|
||||
@Throws(RemoteException::class)
|
||||
get() {
|
||||
|
|
@ -45,22 +45,27 @@ internal class PackageService(
|
|||
// log packages
|
||||
if (Log.isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Got ${packages.size} packages:")
|
||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
||||
Log.i(TAG, it.toString())
|
||||
}
|
||||
logPackages(packages)
|
||||
}
|
||||
|
||||
val eligibleApps =
|
||||
backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray())
|
||||
// As a result of switching to D2D, we can no longer use BackupManager's method
|
||||
// filterAppsEligibleForBackupForUser, because it will not return all of the expected
|
||||
// apps; it is not designed to determine MIGRATION eligibility, only eligibility for
|
||||
// inclusion in *scheduled* BACKUPs, which are implicitly *not* D2D migrations.
|
||||
// None of the other eligibility methods are exposed by AOSP APIs. On the other hand,
|
||||
// the actual backup process properly utilizes OperationType.MIGRATION and performs its
|
||||
// own checks as to whether apps are allowed to be backed up. All we need to do now is
|
||||
// filter out apps that *we* want to be excluded. The system will do the rest later.
|
||||
val requestedApps = packages.filter { settingsManager.isAppAllowedForBackup(it) }
|
||||
|
||||
// log eligible packages
|
||||
// log requested packages
|
||||
if (Log.isLoggable(TAG, INFO)) {
|
||||
Log.i(TAG, "Filtering left ${eligibleApps.size} eligible packages:")
|
||||
logPackages(eligibleApps.toList())
|
||||
Log.i(TAG, "Filtering left ${requestedApps.size} requested packages:")
|
||||
logPackages(requestedApps.toList())
|
||||
}
|
||||
|
||||
// add magic @pm@ package (PACKAGE_MANAGER_SENTINEL) which holds package manager data
|
||||
val packageArray = eligibleApps.toMutableList()
|
||||
val packageArray = requestedApps.toMutableList()
|
||||
packageArray.add(MAGIC_PACKAGE_MANAGER)
|
||||
|
||||
return packageArray.toTypedArray()
|
||||
|
|
@ -159,7 +164,13 @@ internal fun PackageInfo.isSystemApp(): Boolean {
|
|||
|
||||
internal fun PackageInfo.allowsBackup(): Boolean {
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
||||
|
||||
// At backup time, the system will filter out any apps that *it* does not want to be backed up.
|
||||
// Now that we have switched to D2D, *we* generally want to back up as much as possible;
|
||||
// part of the point of D2D is to ignore FLAG_ALLOW_BACKUP (allowsBackup). So, we return true.
|
||||
// See frameworks/base/services/backup/java/com/android/server/backup/utils/
|
||||
// BackupEligibilityRules.java lines 74-81 and 163-167 (tag: android-13.0.0_r8).
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ import com.stevesoltys.seedvault.transport.TRANSPORT_FLAGS
|
|||
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Device name used in AOSP to indicate that a restore set is part of a device-to-device migration.
|
||||
* See getBackupEligibilityRules in frameworks/base/services/backup/java/com/android/server/
|
||||
* backup/restore/ActiveRestoreSession.java. AOSP currently relies on this constant, and it is not
|
||||
* publicly exposed. Framework code indicates they intend to use a flag, instead, in the future.
|
||||
*/
|
||||
internal const val DEVICE_NAME_FOR_D2D_SET = "D2D"
|
||||
|
||||
private data class RestoreCoordinatorState(
|
||||
val token: Long,
|
||||
val packages: Iterator<PackageInfo>,
|
||||
|
|
@ -92,7 +100,8 @@ internal class RestoreCoordinator(
|
|||
**/
|
||||
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||
return getAvailableMetadata()?.map { (_, metadata) ->
|
||||
RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token, TRANSPORT_FLAGS)
|
||||
RestoreSet(metadata.deviceName /* name */, DEVICE_NAME_FOR_D2D_SET /* device */,
|
||||
metadata.token, TRANSPORT_FLAGS)
|
||||
}?.toTypedArray()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package com.stevesoltys.seedvault.settings
|
||||
|
||||
import com.stevesoltys.seedvault.transport.TransportTest
|
||||
import io.mockk.every
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class SettingsManagerTest : TransportTest() {
|
||||
|
||||
private val userExcludedApps = mutableSetOf<String>()
|
||||
|
||||
@Test
|
||||
fun `isAppAllowedForBackup() exempts plugin provider and blacklisted apps`() {
|
||||
val pkgName = packageInfo.packageName
|
||||
|
||||
preparePartiallyMockedSettingsManager()
|
||||
|
||||
settingsManager.pluginProviderPackageName = pkgName
|
||||
assertFalse(settingsManager.isAppAllowedForBackup(pkgName),
|
||||
"Plugin provider must not be allowed for backup")
|
||||
|
||||
settingsManager.pluginProviderPackageName = "new.package"
|
||||
userExcludedApps.add(pkgName)
|
||||
assertFalse(settingsManager.isAppAllowedForBackup(pkgName),
|
||||
"Excluded package must not be allowed for backup")
|
||||
|
||||
userExcludedApps.remove(pkgName)
|
||||
assertTrue(settingsManager.isAppAllowedForBackup(pkgName),
|
||||
"Non-excluded package should be allowed if it is not the plugin provider")
|
||||
}
|
||||
|
||||
private fun preparePartiallyMockedSettingsManager() {
|
||||
// Use our own app exclusion set to avoid the uninitialized set in mocked SettingsManager.
|
||||
every { settingsManager.blacklistedApps } answers { userExcludedApps }
|
||||
|
||||
// Always call isAppAllowedForBackup unmocked rather than an unimplemented mock.
|
||||
every { settingsManager.isAppAllowedForBackup(any()) } answers { callOriginal() }
|
||||
|
||||
// Always call isBackupEnabled unmocked.
|
||||
every { settingsManager.isBackupEnabled(any()) } answers { callOriginal() }
|
||||
|
||||
// Always set the underlying provider name field for use by unmocked isAppAllowedForBackup.
|
||||
every {
|
||||
settingsManager.pluginProviderPackageName = any()
|
||||
} propertyType String::class answers {
|
||||
fieldValue = value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -32,8 +32,6 @@ import io.mockk.mockk
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
|
@ -183,20 +181,6 @@ internal class BackupCoordinatorTest : BackupTest() {
|
|||
verify { metadataOutputStream.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isAppEligibleForBackup() exempts plugin provider and blacklisted apps`() {
|
||||
every {
|
||||
settingsManager.isBackupEnabled(packageInfo.packageName)
|
||||
} returns true andThen false andThen true
|
||||
every {
|
||||
plugin.providerPackageName
|
||||
} returns packageInfo.packageName andThen "new.package" andThen "new.package"
|
||||
|
||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
assertFalse(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
assertTrue(backup.isAppEligibleForBackup(packageInfo, true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearing KV backup data throws`() = runBlocking {
|
||||
every { settingsManager.getToken() } returns token
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
|||
|
||||
val sets = restore.getAvailableRestoreSets() ?: fail()
|
||||
assertEquals(2, sets.size)
|
||||
assertEquals(metadata.deviceName, sets[0].device)
|
||||
assertEquals(DEVICE_NAME_FOR_D2D_SET, sets[0].device)
|
||||
assertEquals(metadata.deviceName, sets[0].name)
|
||||
assertEquals(metadata.token, sets[0].token)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue