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:
t-m-w 2022-11-16 13:08:51 -05:00 committed by Steve Soltys
parent 5d19160602
commit c2f737458c
6 changed files with 46 additions and 35 deletions

View file

@ -143,12 +143,9 @@ internal class BackupCoordinator(
@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
val shouldInclude = packageService.shouldIncludeAppInBackup(packageName)
if (!shouldInclude) Log.i(TAG, "Excluding $packageName from backup.")
return shouldInclude
}
/**

View file

@ -8,7 +8,8 @@ val backupModule = module {
single {
PackageService(
context = androidContext(),
backupManager = get()
settingsManager = get(),
plugin = get()
)
}
single {

View file

@ -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
@ -12,11 +11,12 @@ import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_INSTRUMENTATION
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.os.RemoteException
import android.os.UserHandle
import android.util.Log
import android.util.Log.INFO
import androidx.annotation.WorkerThread
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
@ -28,11 +28,11 @@ private const val LOG_MAX_PACKAGES = 100
*/
internal class PackageService(
private val context: Context,
private val backupManager: IBackupManager,
private val settingsManager: SettingsManager,
private val plugin: StoragePlugin,
) {
private val packageManager: PackageManager = context.packageManager
private val myUserId = UserHandle.myUserId()
val eligiblePackages: Array<String>
@WorkerThread
@ -45,13 +45,16 @@ 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())
// We do not use BackupManager.filterAppsEligibleForBackupForUser because it
// 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
if (Log.isLoggable(TAG, INFO)) {
@ -128,6 +131,14 @@ internal class PackageService(
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>) {
packages.chunked(LOG_MAX_PACKAGES).forEach {
Log.i(TAG, it.toString())
@ -159,7 +170,16 @@ 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
// 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
}
/**

View file

@ -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()
}

View file

@ -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
@ -170,20 +168,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

View file

@ -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)
}