Skip to content

Commit

Permalink
WIP: Also snapshot unchanged APKs
Browse files Browse the repository at this point in the history
  • Loading branch information
grote committed Sep 11, 2024
1 parent b880a87 commit 4689c7c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 42 deletions.
31 changes: 20 additions & 11 deletions app/src/main/java/com/stevesoltys/seedvault/worker/ApkBackup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ import android.util.PackageUtils.computeSha256DigestBytes
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.proto.Snapshot
import com.stevesoltys.seedvault.proto.Snapshot.Apk
import com.stevesoltys.seedvault.proto.Snapshot.Blob
import com.stevesoltys.seedvault.proto.SnapshotKt.split
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.SnapshotManager
import com.stevesoltys.seedvault.transport.backup.AppBackupManager
import com.stevesoltys.seedvault.transport.backup.BackupReceiver
import com.stevesoltys.seedvault.transport.backup.forProto
Expand All @@ -35,7 +36,6 @@ internal class ApkBackup(
private val pm: PackageManager,
private val backupReceiver: BackupReceiver,
private val appBackupManager: AppBackupManager,
private val snapshotManager: SnapshotManager,
private val settingsManager: SettingsManager,
) {

Expand All @@ -50,7 +50,7 @@ internal class ApkBackup(
* @return new [PackageMetadata] if an APK backup was made or null if no backup was made.
*/
@Throws(IOException::class)
suspend fun backupApkIfNecessary(packageInfo: PackageInfo) {
suspend fun backupApkIfNecessary(packageInfo: PackageInfo, latestSnapshot: Snapshot?) {
// do not back up @pm@
val packageName = packageInfo.packageName
if (packageName == MAGIC_PACKAGE_MANAGER) return
Expand Down Expand Up @@ -93,23 +93,33 @@ internal class ApkBackup(

// get info from latest snapshot
val version = packageInfo.longVersionCode
val oldApk = snapshotManager.latestSnapshot?.appsMap?.get(packageName)?.apk
val oldApk = latestSnapshot?.appsMap?.get(packageName)?.apk
val backedUpVersion = oldApk?.versionCode ?: 0L // no version will cause backup

// do not backup if we have the version already and signatures did not change
if (version <= backedUpVersion && !signaturesChanged(oldApk, signatures)) {
val needsBackup = version > backedUpVersion || signaturesChanged(oldApk, signatures)
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."
)
// 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.
// build up chunkMap from old snapshot
val chunkIds = oldApk.splitsList.flatMap {
it.chunkIdsList.map { chunkId -> chunkId.hexFromProto() }
}
val chunkMap = 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, chunkMap)
return
}

// builder for Apk object
val apkBuilder = Snapshot.Apk.newBuilder().apply {
val apkBuilder = Apk.newBuilder().apply {
versionCode = version
pm.getInstallSourceInfo(packageName).installingPackageName?.let {
// protobuf doesn't support null values
Expand Down Expand Up @@ -142,12 +152,11 @@ internal class ApkBackup(
}
val apk = apkBuilder.addAllSplits(splits).build()
snapshotCreator.onApkBackedUp(packageInfo, apk, chunkMap)

Log.d(TAG, "Backed up new APK of $packageName with version ${packageInfo.versionName}.")
}

private fun signaturesChanged(
apk: Snapshot.Apk?,
apk: Apk?,
signatures: List<String>,
): Boolean {
// no signatures counts as them not having changed
Expand All @@ -172,7 +181,7 @@ internal class ApkBackup(
@Throws(IOException::class)
private suspend fun backupSplitApks(
packageInfo: PackageInfo,
chunkMap: MutableMap<String, Snapshot.Blob>,
chunkMap: MutableMap<String, Blob>,
): List<Snapshot.Split> {
check(packageInfo.splitNames != null)
// attention: though not documented, splitSourceDirs can be null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.SnapshotManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.isStopped
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
Expand All @@ -22,6 +23,7 @@ import java.io.IOException
internal class ApkBackupManager(
private val context: Context,
private val settingsManager: SettingsManager,
private val snapshotManager: SnapshotManager,
private val metadataManager: MetadataManager,
private val packageService: PackageService,
private val iconManager: IconManager,
Expand Down Expand Up @@ -99,7 +101,7 @@ internal class ApkBackupManager(
private suspend fun backUpApk(packageInfo: PackageInfo) {
val packageName = packageInfo.packageName
try {
apkBackup.backupApkIfNecessary(packageInfo)
apkBackup.backupApkIfNecessary(packageInfo, snapshotManager.latestSnapshot)
} catch (e: IOException) {
Log.e(TAG, "Error while writing APK for $packageName", e)
if (e.isOutOfSpace()) nm.onInsufficientSpaceError()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ val workerModule = module {
pm = androidContext().packageManager,
backupReceiver = get(),
appBackupManager = get(),
snapshotManager = get(),
settingsManager = get(),
)
}
single {
ApkBackupManager(
context = androidContext(),
settingsManager = get(),
snapshotManager = get(),
metadataManager = get(),
packageService = get(),
apkBackup = get(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
private val apkInstaller: ApkInstaller = mockk()
private val installRestriction: InstallRestriction = mockk()

private val apkBackup =
ApkBackup(pm, backupReceiver, appBackupManager, snapshotManager, settingsManager)
private val apkBackup = ApkBackup(pm, backupReceiver, appBackupManager, settingsManager)
private val apkRestore: ApkRestore = ApkRestore(
context = strictContext,
backupManager = backupManager,
Expand Down Expand Up @@ -153,7 +152,7 @@ internal class ApkBackupRestoreTest : TransportTest() {
snapshotCreator.onApkBackedUp(packageInfo, any<Snapshot.Apk>(), chunkMap)
} just Runs

apkBackup.backupApkIfNecessary(packageInfo)
apkBackup.backupApkIfNecessary(packageInfo, snapshot)

assertArrayEquals(apkBytes, outputStream.toByteArray())
assertArrayEquals(splitBytes, splitOutputStream.toByteArray())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ internal class BackupCoordinatorTest : BackupTest() {
coEvery {
full.performFullBackup(packageInfo, fileDescriptor, 0)
} returns TRANSPORT_OK
coEvery { apkBackup.backupApkIfNecessary(packageInfo) } just Runs
coEvery { apkBackup.backupApkIfNecessary(packageInfo, snapshot) } just Runs

assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))
}
Expand Down Expand Up @@ -286,7 +286,7 @@ internal class BackupCoordinatorTest : BackupTest() {
}

private fun expectApkBackupAndMetadataWrite() {
coEvery { apkBackup.backupApkIfNecessary(packageInfo) } just Runs
coEvery { apkBackup.backupApkIfNecessary(packageInfo, snapshot) } just Runs
every { metadataManager.onApkBackedUp(any(), packageMetadata) } just Runs
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.transport.SnapshotManager
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
Expand All @@ -32,6 +33,7 @@ import org.junit.jupiter.api.Test

internal class ApkBackupManagerTest : TransportTest() {

private val snapshotManager: SnapshotManager = mockk()
private val packageService: PackageService = mockk()
private val apkBackup: ApkBackup = mockk()
private val iconManager: IconManager = mockk()
Expand All @@ -42,6 +44,7 @@ internal class ApkBackupManagerTest : TransportTest() {
private val apkBackupManager = ApkBackupManager(
context = context,
settingsManager = settingsManager,
snapshotManager = snapshotManager,
metadataManager = metadataManager,
packageService = packageService,
iconManager = iconManager,
Expand Down Expand Up @@ -195,23 +198,24 @@ internal class ApkBackupManagerTest : TransportTest() {
every {
nm.onApkBackup(notAllowedPackages[0].packageName, any(), 0, notAllowedPackages.size)
} just Runs
every { snapshotManager.latestSnapshot } returns snapshot
// no backup needed
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[0]) } just Runs
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[0], snapshot) } just Runs
// update notification for second package
every {
nm.onApkBackup(notAllowedPackages[1].packageName, any(), 1, notAllowedPackages.size)
} just Runs
// was backed up, get new packageMetadata
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1]) } just Runs
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], snapshot) } just Runs
every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata) } just Runs

every { nm.onApkBackupDone() } just Runs

apkBackupManager.backup()

coVerify {
apkBackup.backupApkIfNecessary(notAllowedPackages[0])
apkBackup.backupApkIfNecessary(notAllowedPackages[1])
apkBackup.backupApkIfNecessary(notAllowedPackages[0], snapshot)
apkBackup.backupApkIfNecessary(notAllowedPackages[1], snapshot)
}
}

Expand Down
Loading

0 comments on commit 4689c7c

Please sign in to comment.