Make APK backup self-healing
This commit is contained in:
parent
15e8850e5e
commit
4f5199ce27
5 changed files with 80 additions and 14 deletions
|
@ -62,6 +62,15 @@ class BlobCache(
|
||||||
*/
|
*/
|
||||||
operator fun get(chunkId: String): Blob? = blobMap[chunkId]
|
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<String>): Boolean = chunkIds.all { chunkId ->
|
||||||
|
blobMap.containsKey(chunkId)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should get called for all new blobs as soon as they've been saved to the backend.
|
* Should get called for all new blobs as soon as they've been saved to the backend.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -19,6 +19,7 @@ import com.stevesoltys.seedvault.proto.Snapshot.Blob
|
||||||
import com.stevesoltys.seedvault.proto.SnapshotKt.split
|
import com.stevesoltys.seedvault.proto.SnapshotKt.split
|
||||||
import com.stevesoltys.seedvault.repo.AppBackupManager
|
import com.stevesoltys.seedvault.repo.AppBackupManager
|
||||||
import com.stevesoltys.seedvault.repo.BackupReceiver
|
import com.stevesoltys.seedvault.repo.BackupReceiver
|
||||||
|
import com.stevesoltys.seedvault.repo.BlobCache
|
||||||
import com.stevesoltys.seedvault.repo.forProto
|
import com.stevesoltys.seedvault.repo.forProto
|
||||||
import com.stevesoltys.seedvault.repo.hexFromProto
|
import com.stevesoltys.seedvault.repo.hexFromProto
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
|
@ -37,6 +38,7 @@ internal class ApkBackup(
|
||||||
private val backupReceiver: BackupReceiver,
|
private val backupReceiver: BackupReceiver,
|
||||||
private val appBackupManager: AppBackupManager,
|
private val appBackupManager: AppBackupManager,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
|
private val blobCache: BlobCache,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val snapshotCreator
|
private val snapshotCreator
|
||||||
|
@ -101,22 +103,26 @@ internal class ApkBackup(
|
||||||
if (!needsBackup && oldApk != null) {
|
if (!needsBackup && oldApk != null) {
|
||||||
// We could also check if there are new feature module splits to back up,
|
// 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.
|
// 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 {
|
val chunkIds = oldApk.splitsList.flatMap {
|
||||||
it.chunkIdsList.map { chunkId -> chunkId.hexFromProto() }
|
it.chunkIdsList.map { chunkId -> chunkId.hexFromProto() }
|
||||||
}
|
}
|
||||||
val blobMap = chunkIds.associateWith { chunkId ->
|
if (blobCache.containsAll(chunkIds)) {
|
||||||
latestSnapshot.blobsMap[chunkId] ?: error("Missing blob for $chunkId")
|
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
|
// builder for Apk object
|
||||||
|
|
|
@ -33,6 +33,7 @@ val workerModule = module {
|
||||||
backupReceiver = get(),
|
backupReceiver = get(),
|
||||||
appBackupManager = get(),
|
appBackupManager = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
|
blobCache = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single {
|
single {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
import com.stevesoltys.seedvault.proto.Snapshot
|
import com.stevesoltys.seedvault.proto.Snapshot
|
||||||
import com.stevesoltys.seedvault.repo.AppBackupManager
|
import com.stevesoltys.seedvault.repo.AppBackupManager
|
||||||
import com.stevesoltys.seedvault.repo.BackupReceiver
|
import com.stevesoltys.seedvault.repo.BackupReceiver
|
||||||
|
import com.stevesoltys.seedvault.repo.BlobCache
|
||||||
import com.stevesoltys.seedvault.repo.Loader
|
import com.stevesoltys.seedvault.repo.Loader
|
||||||
import com.stevesoltys.seedvault.repo.SnapshotCreator
|
import com.stevesoltys.seedvault.repo.SnapshotCreator
|
||||||
import com.stevesoltys.seedvault.repo.SnapshotManager
|
import com.stevesoltys.seedvault.repo.SnapshotManager
|
||||||
|
@ -70,6 +71,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
||||||
private val backupStateManager: BackupStateManager = mockk()
|
private val backupStateManager: BackupStateManager = mockk()
|
||||||
private val backupReceiver: BackupReceiver = mockk()
|
private val backupReceiver: BackupReceiver = mockk()
|
||||||
private val appBackupManager: AppBackupManager = mockk()
|
private val appBackupManager: AppBackupManager = mockk()
|
||||||
|
private val blobCache: BlobCache = mockk()
|
||||||
private val snapshotManager: SnapshotManager = mockk()
|
private val snapshotManager: SnapshotManager = mockk()
|
||||||
private val snapshotCreator: SnapshotCreator = mockk()
|
private val snapshotCreator: SnapshotCreator = mockk()
|
||||||
private val loader: Loader = mockk()
|
private val loader: Loader = mockk()
|
||||||
|
@ -81,7 +83,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
|
||||||
private val apkInstaller: ApkInstaller = mockk()
|
private val apkInstaller: ApkInstaller = mockk()
|
||||||
private val installRestriction: InstallRestriction = 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(
|
private val apkRestore: ApkRestore = ApkRestore(
|
||||||
context = strictContext,
|
context = strictContext,
|
||||||
backupManager = backupManager,
|
backupManager = backupManager,
|
||||||
|
|
|
@ -23,7 +23,9 @@ import com.stevesoltys.seedvault.proto.copy
|
||||||
import com.stevesoltys.seedvault.repo.AppBackupManager
|
import com.stevesoltys.seedvault.repo.AppBackupManager
|
||||||
import com.stevesoltys.seedvault.repo.BackupData
|
import com.stevesoltys.seedvault.repo.BackupData
|
||||||
import com.stevesoltys.seedvault.repo.BackupReceiver
|
import com.stevesoltys.seedvault.repo.BackupReceiver
|
||||||
|
import com.stevesoltys.seedvault.repo.BlobCache
|
||||||
import com.stevesoltys.seedvault.repo.SnapshotCreator
|
import com.stevesoltys.seedvault.repo.SnapshotCreator
|
||||||
|
import com.stevesoltys.seedvault.repo.hexFromProto
|
||||||
import com.stevesoltys.seedvault.transport.backup.BackupTest
|
import com.stevesoltys.seedvault.transport.backup.BackupTest
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -51,9 +53,11 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
private val pm: PackageManager = mockk()
|
private val pm: PackageManager = mockk()
|
||||||
private val backupReceiver: BackupReceiver = mockk()
|
private val backupReceiver: BackupReceiver = mockk()
|
||||||
private val appBackupManager: AppBackupManager = mockk()
|
private val appBackupManager: AppBackupManager = mockk()
|
||||||
|
private val blobCache: BlobCache = mockk()
|
||||||
private val snapshotCreator: SnapshotCreator = 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 signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||||
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
||||||
|
@ -110,7 +114,9 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
val apk = apk.copy { versionCode = packageInfo.longVersionCode }
|
val apk = apk.copy { versionCode = packageInfo.longVersionCode }
|
||||||
val app = app { this.apk = apk }
|
val app = app { this.apk = apk }
|
||||||
val s = snapshot.copy { apps.put(packageName, app) }
|
val s = snapshot.copy { apps.put(packageName, app) }
|
||||||
|
val chunkIds = apk.splitsList.flatMap { it.chunkIdsList.hexFromProto() }
|
||||||
expectChecks()
|
expectChecks()
|
||||||
|
every { blobCache.containsAll(chunkIds) } returns true
|
||||||
every {
|
every {
|
||||||
snapshotCreator.onApkBackedUp(packageInfo, apk, blobMap)
|
snapshotCreator.onApkBackedUp(packageInfo, apk, blobMap)
|
||||||
} just Runs
|
} 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<Snapshot.Apk> {
|
||||||
|
it.installer == apk.installer
|
||||||
|
}, apkBackupData.blobMap)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
|
apkBackup.backupApkIfNecessary(packageInfo, s)
|
||||||
|
|
||||||
|
coVerify {
|
||||||
|
backupReceiver.readFromStream("APK backup $packageName ", any())
|
||||||
|
snapshotCreator.onApkBackedUp(packageInfo, match<Snapshot.Apk> {
|
||||||
|
it.installer == apk.installer
|
||||||
|
}, apkBackupData.blobMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `throws exception when APK doesn't exist`() {
|
fun `throws exception when APK doesn't exist`() {
|
||||||
packageInfo.applicationInfo!!.sourceDir = "/tmp/doesNotExist"
|
packageInfo.applicationInfo!!.sourceDir = "/tmp/doesNotExist"
|
||||||
|
|
Loading…
Reference in a new issue