From 4f5199ce27eaebd0dc5c044201fe808cbd0f0cf0 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 30 Oct 2024 09:58:33 -0300 Subject: [PATCH] Make APK backup self-healing --- .../stevesoltys/seedvault/repo/BlobCache.kt | 9 ++++ .../stevesoltys/seedvault/worker/ApkBackup.kt | 30 +++++++----- .../seedvault/worker/WorkerModule.kt | 1 + .../restore/install/ApkBackupRestoreTest.kt | 5 +- .../seedvault/worker/ApkBackupTest.kt | 49 ++++++++++++++++++- 5 files changed, 80 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt b/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt index 16085512..478c77e5 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/repo/BlobCache.kt @@ -62,6 +62,15 @@ class BlobCache( */ operator fun get(chunkId: String): Blob? = blobMap[chunkId] + /** + * Should only be called after [populateCache] has returned. + * + * @return true if all [chunkIds] are in cache, or false if one or more is missing. + */ + fun containsAll(chunkIds: List): Boolean = chunkIds.all { chunkId -> + blobMap.containsKey(chunkId) + } + /** * Should get called for all new blobs as soon as they've been saved to the backend. */ diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt index 7b945845..b4d8cb5c 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt @@ -19,6 +19,7 @@ import com.stevesoltys.seedvault.proto.Snapshot.Blob import com.stevesoltys.seedvault.proto.SnapshotKt.split import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.BackupReceiver +import com.stevesoltys.seedvault.repo.BlobCache import com.stevesoltys.seedvault.repo.forProto import com.stevesoltys.seedvault.repo.hexFromProto import com.stevesoltys.seedvault.settings.SettingsManager @@ -37,6 +38,7 @@ internal class ApkBackup( private val backupReceiver: BackupReceiver, private val appBackupManager: AppBackupManager, private val settingsManager: SettingsManager, + private val blobCache: BlobCache, ) { private val snapshotCreator @@ -101,22 +103,26 @@ internal class ApkBackup( if (!needsBackup && oldApk != null) { // We could also check if there are new feature module splits to back up, // but we rely on the app themselves to re-download those, if needed after restore. - Log.d( - TAG, "Package $packageName with version $version" + - " already has a backup ($backedUpVersion)" + - " with the same signature. Not backing it up." - ) - // build up blobMap from old snapshot + val chunkIds = oldApk.splitsList.flatMap { it.chunkIdsList.map { chunkId -> chunkId.hexFromProto() } } - val blobMap = chunkIds.associateWith { chunkId -> - latestSnapshot.blobsMap[chunkId] ?: error("Missing blob for $chunkId") + if (blobCache.containsAll(chunkIds)) { + Log.d( + TAG, "Package $packageName with version $version" + + " already has a backup ($backedUpVersion)" + + " with the same signature. Not backing it up." + ) + // all blobs are cached, i.e. still on backend, so no new backup needed + val blobMap = chunkIds.associateWith { chunkId -> + latestSnapshot.blobsMap[chunkId] ?: error("Missing blob for $chunkId") + } + // important: add old APK to snapshot or it wouldn't be part of backup + snapshotCreator.onApkBackedUp(packageInfo, oldApk, blobMap) + return + } else { + Log.w(TAG, "Blobs for APKs of $packageName have issues in backend. Fixing...") } - // TODO could also check if all blobs are (still) available in BlobCache - // important: add old APK to snapshot or it wouldn't be part of backup - snapshotCreator.onApkBackedUp(packageInfo, oldApk, blobMap) - return } // builder for Apk object diff --git a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt index 40dfd4c7..1b395356 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/worker/WorkerModule.kt @@ -33,6 +33,7 @@ val workerModule = module { backupReceiver = get(), appBackupManager = get(), settingsManager = get(), + blobCache = get(), ) } single { diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt index 874b3ed2..a7579b1c 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt @@ -24,6 +24,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadataMap import com.stevesoltys.seedvault.proto.Snapshot import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.BackupReceiver +import com.stevesoltys.seedvault.repo.BlobCache import com.stevesoltys.seedvault.repo.Loader import com.stevesoltys.seedvault.repo.SnapshotCreator import com.stevesoltys.seedvault.repo.SnapshotManager @@ -70,6 +71,7 @@ internal class ApkBackupRestoreTest : TransportTest() { private val backupStateManager: BackupStateManager = mockk() private val backupReceiver: BackupReceiver = mockk() private val appBackupManager: AppBackupManager = mockk() + private val blobCache: BlobCache = mockk() private val snapshotManager: SnapshotManager = mockk() private val snapshotCreator: SnapshotCreator = mockk() private val loader: Loader = mockk() @@ -81,7 +83,8 @@ internal class ApkBackupRestoreTest : TransportTest() { private val apkInstaller: ApkInstaller = mockk() private val installRestriction: InstallRestriction = mockk() - private val apkBackup = ApkBackup(pm, backupReceiver, appBackupManager, settingsManager) + private val apkBackup = + ApkBackup(pm, backupReceiver, appBackupManager, settingsManager, blobCache) private val apkRestore: ApkRestore = ApkRestore( context = strictContext, backupManager = backupManager, diff --git a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt index 2a2011da..30957fde 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/worker/ApkBackupTest.kt @@ -23,7 +23,9 @@ import com.stevesoltys.seedvault.proto.copy import com.stevesoltys.seedvault.repo.AppBackupManager import com.stevesoltys.seedvault.repo.BackupData import com.stevesoltys.seedvault.repo.BackupReceiver +import com.stevesoltys.seedvault.repo.BlobCache import com.stevesoltys.seedvault.repo.SnapshotCreator +import com.stevesoltys.seedvault.repo.hexFromProto import com.stevesoltys.seedvault.transport.backup.BackupTest import io.mockk.Runs import io.mockk.coEvery @@ -51,9 +53,11 @@ internal class ApkBackupTest : BackupTest() { private val pm: PackageManager = mockk() private val backupReceiver: BackupReceiver = mockk() private val appBackupManager: AppBackupManager = mockk() + private val blobCache: BlobCache = mockk() private val snapshotCreator: SnapshotCreator = mockk() - private val apkBackup = ApkBackup(pm, backupReceiver, appBackupManager, settingsManager) + private val apkBackup = + ApkBackup(pm, backupReceiver, appBackupManager, settingsManager, blobCache) private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03) private val signatureHash = byteArrayOf(0x03, 0x02, 0x01) @@ -110,7 +114,9 @@ internal class ApkBackupTest : BackupTest() { val apk = apk.copy { versionCode = packageInfo.longVersionCode } val app = app { this.apk = apk } val s = snapshot.copy { apps.put(packageName, app) } + val chunkIds = apk.splitsList.flatMap { it.chunkIdsList.hexFromProto() } expectChecks() + every { blobCache.containsAll(chunkIds) } returns true every { snapshotCreator.onApkBackedUp(packageInfo, apk, blobMap) } just Runs @@ -162,6 +168,47 @@ internal class ApkBackupTest : BackupTest() { } } + @Test + fun `does back up the same version when blobs are missing from cache`(@TempDir tmpDir: Path) = + runBlocking { + val tmpFile = File(tmpDir.toAbsolutePath().toString()) + packageInfo.applicationInfo!!.sourceDir = File(tmpFile, "test.apk").apply { + assertTrue(createNewFile()) + }.absolutePath + val apk = apk.copy { + versionCode = packageInfo.longVersionCode + splits.clear() + splits.add(baseSplit) + } + val app = app { this.apk = apk } + val s = snapshot.copy { apps.put(packageName, app) } + val chunkIds = apk.splitsList.flatMap { it.chunkIdsList.hexFromProto() } + expectChecks() + every { blobCache.containsAll(chunkIds) } returns false // blobs missing here + + every { + pm.getInstallSourceInfo(packageInfo.packageName) + } returns InstallSourceInfo(null, null, null, apk.installer) + coEvery { + backupReceiver.readFromStream("APK backup $packageName ", any()) + } returns apkBackupData + + every { + snapshotCreator.onApkBackedUp(packageInfo, match { + it.installer == apk.installer + }, apkBackupData.blobMap) + } just Runs + + apkBackup.backupApkIfNecessary(packageInfo, s) + + coVerify { + backupReceiver.readFromStream("APK backup $packageName ", any()) + snapshotCreator.onApkBackedUp(packageInfo, match { + it.installer == apk.installer + }, apkBackupData.blobMap) + } + } + @Test fun `throws exception when APK doesn't exist`() { packageInfo.applicationInfo!!.sourceDir = "/tmp/doesNotExist"