Initial support for backup of D2D-only apps
Allow backup of apps that would otherwise only support device-to-device migration. This is an initial-support patch to help determine the viability of this approach. Known issues / TODO: * System-scheduled backups will not handle D2D-only apps, unless accompanied by a framework change forcing OperationType.MIGRATION. Backups triggered by the connection of a USB device or by Seedvault's StorageBackupService (files) scheduling are not affected, so they *will* back up D2D-only apps as expected; otherwise, the user may need to perform a backup manually via Backup Now. * Apps with `allowBackup="false"` will appear in Backup Status under "Installed Apps" rather than "Apps that do not allow data backup", and their status will always be blank until they have been backed up. If they are not eligible for migration, it will never change. Other notes: * The unit test for excluding the Storage Plugin provider from backups was discussed, deemed unnecessary, and removed. Co-authored-by: Oliver Scott <olivercscott@gmail.com> Change-Id: I5a23d68be66f7d8ed755f2bccb9570ab7be49356
This commit is contained in:
parent
5d19160602
commit
c2f737458c
6 changed files with 46 additions and 35 deletions
|
@ -143,12 +143,9 @@ internal class BackupCoordinator(
|
||||||
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val packageName = targetPackage.packageName
|
val packageName = targetPackage.packageName
|
||||||
// Check that the app is not blacklisted by the user
|
val shouldInclude = packageService.shouldIncludeAppInBackup(packageName)
|
||||||
val enabled = settingsManager.isBackupEnabled(packageName)
|
if (!shouldInclude) Log.i(TAG, "Excluding $packageName from backup.")
|
||||||
if (!enabled) Log.w(TAG, "Backup of $packageName disabled by user.")
|
return shouldInclude
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,7 +8,8 @@ val backupModule = module {
|
||||||
single {
|
single {
|
||||||
PackageService(
|
PackageService(
|
||||||
context = androidContext(),
|
context = androidContext(),
|
||||||
backupManager = get()
|
settingsManager = get(),
|
||||||
|
plugin = get()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
|
||||||
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
import android.content.pm.ApplicationInfo.FLAG_STOPPED
|
||||||
|
@ -12,11 +11,12 @@ import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.GET_INSTRUMENTATION
|
import android.content.pm.PackageManager.GET_INSTRUMENTATION
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.os.UserHandle
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Log.INFO
|
import android.util.Log.INFO
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
|
import com.stevesoltys.seedvault.plugins.StoragePlugin
|
||||||
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
|
||||||
private val TAG = PackageService::class.java.simpleName
|
private val TAG = PackageService::class.java.simpleName
|
||||||
|
|
||||||
|
@ -28,11 +28,11 @@ private const val LOG_MAX_PACKAGES = 100
|
||||||
*/
|
*/
|
||||||
internal class PackageService(
|
internal class PackageService(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val backupManager: IBackupManager,
|
private val settingsManager: SettingsManager,
|
||||||
|
private val plugin: StoragePlugin,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
private val packageManager: PackageManager = context.packageManager
|
||||||
private val myUserId = UserHandle.myUserId()
|
|
||||||
|
|
||||||
val eligiblePackages: Array<String>
|
val eligiblePackages: Array<String>
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
|
@ -45,13 +45,16 @@ internal class PackageService(
|
||||||
// log packages
|
// log packages
|
||||||
if (Log.isLoggable(TAG, INFO)) {
|
if (Log.isLoggable(TAG, INFO)) {
|
||||||
Log.i(TAG, "Got ${packages.size} packages:")
|
Log.i(TAG, "Got ${packages.size} packages:")
|
||||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
logPackages(packages)
|
||||||
Log.i(TAG, it.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val eligibleApps =
|
// We do not use BackupManager.filterAppsEligibleForBackupForUser because it
|
||||||
backupManager.filterAppsEligibleForBackupForUser(myUserId, packages.toTypedArray())
|
// always makes its determinations based on OperationType.BACKUP, never based on
|
||||||
|
// OperationType.MIGRATION, and there are no alternative publicly-available APIs.
|
||||||
|
// We don't need to use it, here, either; during a backup or migration, the system
|
||||||
|
// will perform its own eligibility checks regardless. We merely need to filter out
|
||||||
|
// apps that we, or the user, want to exclude.
|
||||||
|
val eligibleApps = packages.filter(::shouldIncludeAppInBackup)
|
||||||
|
|
||||||
// log eligible packages
|
// log eligible packages
|
||||||
if (Log.isLoggable(TAG, INFO)) {
|
if (Log.isLoggable(TAG, INFO)) {
|
||||||
|
@ -128,6 +131,14 @@ internal class PackageService(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shouldIncludeAppInBackup(packageName: String): Boolean {
|
||||||
|
// Check that the app is not excluded by user preference
|
||||||
|
val enabled = settingsManager.isBackupEnabled(packageName)
|
||||||
|
// We also 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 != plugin.providerPackageName
|
||||||
|
}
|
||||||
|
|
||||||
private fun logPackages(packages: List<String>) {
|
private fun logPackages(packages: List<String>) {
|
||||||
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
packages.chunked(LOG_MAX_PACKAGES).forEach {
|
||||||
Log.i(TAG, it.toString())
|
Log.i(TAG, it.toString())
|
||||||
|
@ -159,7 +170,16 @@ internal fun PackageInfo.isSystemApp(): Boolean {
|
||||||
|
|
||||||
internal fun PackageInfo.allowsBackup(): Boolean {
|
internal fun PackageInfo.allowsBackup(): Boolean {
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
|
||||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0
|
|
||||||
|
// TODO: Consider ways of replicating the system's logic so that the user can have advance
|
||||||
|
// knowledge of apps that the system will exclude, particularly apps targeting SDK 30 or below.
|
||||||
|
|
||||||
|
// 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 com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
import java.io.IOException
|
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(
|
private data class RestoreCoordinatorState(
|
||||||
val token: Long,
|
val token: Long,
|
||||||
val packages: Iterator<PackageInfo>,
|
val packages: Iterator<PackageInfo>,
|
||||||
|
@ -92,7 +100,8 @@ internal class RestoreCoordinator(
|
||||||
**/
|
**/
|
||||||
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
suspend fun getAvailableRestoreSets(): Array<RestoreSet>? {
|
||||||
return getAvailableMetadata()?.map { (_, metadata) ->
|
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()
|
}?.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,6 @@ import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
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 org.junit.jupiter.api.Test
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
@ -170,20 +168,6 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
verify { metadataOutputStream.close() }
|
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
|
@Test
|
||||||
fun `clearing KV backup data throws`() = runBlocking {
|
fun `clearing KV backup data throws`() = runBlocking {
|
||||||
every { settingsManager.getToken() } returns token
|
every { settingsManager.getToken() } returns token
|
||||||
|
|
|
@ -87,7 +87,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
val sets = restore.getAvailableRestoreSets() ?: fail()
|
val sets = restore.getAvailableRestoreSets() ?: fail()
|
||||||
assertEquals(2, sets.size)
|
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.deviceName, sets[0].name)
|
||||||
assertEquals(metadata.token, sets[0].token)
|
assertEquals(metadata.token, sets[0].token)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue