From a0e3474783a2862b9fd9856111ba91eb5811a875 Mon Sep 17 00:00:00 2001 From: t-m-w <7275539+t-m-w@users.noreply.github.com> Date: Tue, 22 Nov 2022 13:14:30 -0500 Subject: [PATCH] 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 --- .../transport/backup/BackupCoordinator.kt | 69 ++++++++++++++----- .../transport/backup/BackupModule.kt | 3 +- .../transport/CoordinatorIntegrationTest.kt | 10 ++- .../transport/backup/BackupCoordinatorTest.kt | 9 ++- 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index 5d791fc4..0390d7f3 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -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,27 +117,33 @@ 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 { - val token = settingsManager.getToken() - if (token == null) { - Log.i(TAG, "No RestoreSet started, initialization is no-op.") - } else { - Log.i(TAG, "Initialize Device!") - plugin.initializeDevice() - Log.d(TAG, "Resetting backup metadata for token $token...") - plugin.getMetadataOutputStream().use { - metadataManager.onDeviceInitialization(token, it) + 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.") + } else { + Log.i(TAG, "Initialize Device!") + plugin.initializeDevice() + Log.d(TAG, "Resetting backup metadata for token $token...") + plugin.getMetadataOutputStream().use { + metadataManager.onDeviceInitialization(token, it) + } } + // [finishBackup] will only be called when we return [TRANSPORT_OK] here + // so we remember that we initialized successfully + state.calledInitialize = true + TRANSPORT_OK + } 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() + } + TRANSPORT_ERROR } - // [finishBackup] will only be called when we return [TRANSPORT_OK] here - // so we remember that we initialized successfully - state.calledInitialize = true - TRANSPORT_OK - } 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() - TRANSPORT_ERROR } fun isAppEligibleForBackup( @@ -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 + } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt index 8be00ac6..8437100d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt @@ -48,7 +48,8 @@ val backupModule = module { packageService = get(), metadataManager = get(), settingsManager = get(), - nm = get() + nm = get(), + backupManager = get(), ) } } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 824230e8..7d6d6095 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -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() 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( diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index d5facae7..314a57ea 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -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() private val packageService: PackageService = mockk() private val notificationManager = mockk() + private val backupManager = mockk() + + 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()