Preserve backups when disabling "Backup my apps"

Ignore AOSP's attempt to wipe backup data when backups are disabled.

Remaining quirks:
* When backups are re-enabled and another backup is started, the system
  will call initializeDevice, and Seedvault will generate a new backup
  salt and start the backup from scratch. This was already the case,
  and this patch does not change that.

Issue: seedvault-app/seedvault#476
Change-Id: I6ab41a885fcf7c4143814ebe849b8263a4f6e595
This commit is contained in:
t-m-w 2022-11-22 13:14:30 -05:00
parent 271b9a560f
commit a0e3474783
4 changed files with 69 additions and 22 deletions

View file

@ -10,6 +10,7 @@ import android.app.backup.BackupTransport.TRANSPORT_NOT_INITIALIZED
import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.app.backup.IBackupManager
import android.app.backup.RestoreSet
import android.content.Context
import android.content.pm.PackageInfo
@ -72,6 +73,7 @@ internal class BackupCoordinator(
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager,
private val backupManager: IBackupManager,
) {
private val state = CoordinatorState(
@ -115,7 +117,10 @@ internal class BackupCoordinator(
* @return One of [TRANSPORT_OK] (OK so far) or
* [TRANSPORT_ERROR] (to retry following network error or other failure).
*/
suspend fun initializeDevice(): Int = try {
suspend fun initializeDevice(): Int {
if (shouldIgnoreBackupManager("initializeDevice")) return TRANSPORT_OK
return try {
val token = settingsManager.getToken()
if (token == null) {
Log.i(TAG, "No RestoreSet started, initialization is no-op.")
@ -134,9 +139,12 @@ internal class BackupCoordinator(
} catch (e: IOException) {
Log.e(TAG, "Error initializing device", e)
// Show error notification if we needed init or were ready for backups
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) nm.onBackupError()
if (metadataManager.requiresInit || settingsManager.canDoBackupNow()) {
nm.onBackupError()
}
TRANSPORT_ERROR
}
}
fun isAppEligibleForBackup(
targetPackage: PackageInfo,
@ -232,6 +240,8 @@ internal class BackupCoordinator(
data: ParcelFileDescriptor,
flags: Int,
): Int {
if (shouldIgnoreBackupManager("performIncrementalBackup")) return TRANSPORT_OK
state.cancelReason = UNKNOWN_ERROR
if (metadataManager.requiresInit) {
// start a new restore set to upgrade from legacy format
@ -282,6 +292,8 @@ internal class BackupCoordinator(
fileDescriptor: ParcelFileDescriptor,
flags: Int,
): Int {
if (shouldIgnoreBackupManager("performFullBackup")) return TRANSPORT_OK
state.cancelReason = UNKNOWN_ERROR
val token = settingsManager.getToken() ?: error("no token in performFullBackup")
val salt = metadataManager.salt
@ -304,6 +316,8 @@ internal class BackupCoordinator(
* It needs to tear down any ongoing backup state here.
*/
suspend fun cancelFullBackup() {
if (shouldIgnoreBackupManager("cancelFullBackup")) return
val packageInfo = full.getCurrentPackage()
?: throw AssertionError("Cancelling full backup, but no current package")
Log.i(
@ -359,6 +373,7 @@ internal class BackupCoordinator(
* @return the same error codes as [performIncrementalBackup] or [performFullBackup].
*/
suspend fun finishBackup(): Int = when {
shouldIgnoreBackupManager("finishBackup") -> TRANSPORT_OK
kv.hasState() -> {
check(!full.hasState()) {
"K/V backup has state, but full backup has dangling state as well"
@ -510,4 +525,20 @@ internal class BackupCoordinator(
return getOutputStream(t, FILE_BACKUP_METADATA)
}
/**
* Check whether or not backup is enabled, logging a warning if it is not.
* When backup is disabled, the system treats this as an opt out and attempts to start
* a wipe of backup data. (See frameworks/base/services/backup/java/com/android/server/backup/
* UserBackupManagerService.java, lines 3195-3197, tag android-13.0.0_r8.)
* This check exists to ensure that backups are not altered when backup is turned off.
*
* @param methodName the calling method to be included in logging.
* @return true if the backup service is disabled, and false if it is not.
*/
private fun shouldIgnoreBackupManager(methodName: String): Boolean =
if (!backupManager.isBackupEnabled) {
Log.w(TAG, "Ignoring call to $methodName while backup is not enabled")
true
} else false
}

View file

@ -48,7 +48,8 @@ val backupModule = module {
packageService = get(),
metadataManager = get(),
settingsManager = get(),
nm = get()
nm = get(),
backupManager = get(),
)
}
}

View file

@ -4,6 +4,7 @@ import android.app.backup.BackupDataInput
import android.app.backup.BackupDataOutput
import android.app.backup.BackupTransport.NO_MORE_DATA
import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.IBackupManager
import android.app.backup.RestoreDescription
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
import android.os.ParcelFileDescriptor
@ -68,6 +69,12 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val fullBackup = FullBackup(backupPlugin, settingsManager, inputFactory, cryptoImpl)
private val apkBackup = mockk<ApkBackup>()
private val packageService: PackageService = mockk()
private val backupManager: IBackupManager = mockk()
init {
every { backupManager.isBackupEnabled } returns true
}
private val backup = BackupCoordinator(
context,
backupPlugin,
@ -78,7 +85,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
packageService,
metadataManager,
settingsManager,
notificationManager
notificationManager,
backupManager,
)
private val kvRestore = KVRestore(

View file

@ -5,6 +5,7 @@ import android.app.backup.BackupTransport.TRANSPORT_NOT_INITIALIZED
import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.app.backup.IBackupManager
import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.content.pm.PackageInfo
import android.net.Uri
@ -48,6 +49,11 @@ internal class BackupCoordinatorTest : BackupTest() {
private val apkBackup = mockk<ApkBackup>()
private val packageService: PackageService = mockk()
private val notificationManager = mockk<BackupNotificationManager>()
private val backupManager = mockk<IBackupManager>()
init {
every { backupManager.isBackupEnabled } returns true
}
private val backup = BackupCoordinator(
context,
@ -59,7 +65,8 @@ internal class BackupCoordinatorTest : BackupTest() {
packageService,
metadataManager,
settingsManager,
notificationManager
notificationManager,
backupManager,
)
private val metadataOutputStream = mockk<OutputStream>()