From 7702fb7bd8b09c8d08a051154df47b4d7533c0f3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 16 Sep 2024 16:28:11 -0300 Subject: [PATCH] Polish SnapshotCreator and write tests --- .../stevesoltys/seedvault/proto/SnapshotKt.kt | 124 ++++++++-------- .../seedvault/metadata/Metadata.kt | 3 +- .../seedvault/metadata/MetadataManager.kt | 19 --- .../seedvault/metadata/MetadataModule.kt | 2 +- .../transport/backup/SnapshotCreator.kt | 52 +++++-- app/src/main/proto/snapshot.proto | 28 ++-- .../seedvault/metadata/MetadataManagerTest.kt | 10 -- .../transport/backup/SnapshotCreatorTest.kt | 134 ++++++++++++++++++ 8 files changed, 252 insertions(+), 120 deletions(-) create mode 100644 app/src/test/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreatorTest.kt diff --git a/app/build/generated/source/proto/debug/kotlin/com/stevesoltys/seedvault/proto/SnapshotKt.kt b/app/build/generated/source/proto/debug/kotlin/com/stevesoltys/seedvault/proto/SnapshotKt.kt index e9ec0f04..f5141adc 100644 --- a/app/build/generated/source/proto/debug/kotlin/com/stevesoltys/seedvault/proto/SnapshotKt.kt +++ b/app/build/generated/source/proto/debug/kotlin/com/stevesoltys/seedvault/proto/SnapshotKt.kt @@ -74,7 +74,24 @@ public object SnapshotKt { } /** - * string androidId = 4; + * string user = 4; + */ + public var user: kotlin.String + @JvmName("getUser") + get() = _builder.getUser() + @JvmName("setUser") + set(value) { + _builder.setUser(value) + } + /** + * string user = 4; + */ + public fun clearUser() { + _builder.clearUser() + } + + /** + * string androidId = 5; */ public var androidId: kotlin.String @JvmName("getAndroidId") @@ -84,14 +101,14 @@ public object SnapshotKt { _builder.setAndroidId(value) } /** - * string androidId = 4; + * string androidId = 5; */ public fun clearAndroidId() { _builder.clearAndroidId() } /** - * uint32 sdkInt = 5; + * uint32 sdkInt = 6; */ public var sdkInt: kotlin.Int @JvmName("getSdkInt") @@ -101,14 +118,14 @@ public object SnapshotKt { _builder.setSdkInt(value) } /** - * uint32 sdkInt = 5; + * uint32 sdkInt = 6; */ public fun clearSdkInt() { _builder.clearSdkInt() } /** - * string androidIncremental = 6; + * string androidIncremental = 7; */ public var androidIncremental: kotlin.String @JvmName("getAndroidIncremental") @@ -118,14 +135,14 @@ public object SnapshotKt { _builder.setAndroidIncremental(value) } /** - * string androidIncremental = 6; + * string androidIncremental = 7; */ public fun clearAndroidIncremental() { _builder.clearAndroidIncremental() } /** - * bool d2d = 7; + * bool d2d = 8; */ public var d2D: kotlin.Boolean @JvmName("getD2D") @@ -135,7 +152,7 @@ public object SnapshotKt { _builder.setD2D(value) } /** - * bool d2d = 7; + * bool d2d = 8; */ public fun clearD2D() { _builder.clearD2D() @@ -148,7 +165,7 @@ public object SnapshotKt { @kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class) public class AppsProxy private constructor() : com.google.protobuf.kotlin.DslProxy() /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 8; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 9; */ public val apps: com.google.protobuf.kotlin.DslMap @kotlin.jvm.JvmSynthetic @@ -157,7 +174,7 @@ public object SnapshotKt { _builder.getAppsMap() ) /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 8; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 9; */ @JvmName("putApps") public fun com.google.protobuf.kotlin.DslMap @@ -165,7 +182,7 @@ public object SnapshotKt { _builder.putApps(key, value) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 8; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 9; */ @kotlin.jvm.JvmSynthetic @JvmName("setApps") @@ -175,7 +192,7 @@ public object SnapshotKt { put(key, value) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 8; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 9; */ @kotlin.jvm.JvmSynthetic @JvmName("removeApps") @@ -184,7 +201,7 @@ public object SnapshotKt { _builder.removeApps(key) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 8; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 9; */ @kotlin.jvm.JvmSynthetic @JvmName("putAllApps") @@ -193,7 +210,7 @@ public object SnapshotKt { _builder.putAllApps(map) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 8; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.App> apps = 9; */ @kotlin.jvm.JvmSynthetic @JvmName("clearApps") @@ -209,7 +226,7 @@ public object SnapshotKt { @kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class) public class IconChunkIdsProxy private constructor() : com.google.protobuf.kotlin.DslProxy() /** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; */ public val iconChunkIds: com.google.protobuf.kotlin.DslList @kotlin.jvm.JvmSynthetic @@ -217,7 +234,7 @@ public object SnapshotKt { _builder.getIconChunkIdsList() ) /** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; * @param value The iconChunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -225,7 +242,7 @@ public object SnapshotKt { public fun com.google.protobuf.kotlin.DslList.add(value: com.google.protobuf.ByteString) { _builder.addIconChunkIds(value) }/** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; * @param value The iconChunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -234,7 +251,7 @@ public object SnapshotKt { public inline operator fun com.google.protobuf.kotlin.DslList.plusAssign(value: com.google.protobuf.ByteString) { add(value) }/** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; * @param values The iconChunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -242,7 +259,7 @@ public object SnapshotKt { public fun com.google.protobuf.kotlin.DslList.addAll(values: kotlin.collections.Iterable) { _builder.addAllIconChunkIds(values) }/** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; * @param values The iconChunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -251,7 +268,7 @@ public object SnapshotKt { public inline operator fun com.google.protobuf.kotlin.DslList.plusAssign(values: kotlin.collections.Iterable) { addAll(values) }/** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; * @param index The index to set the value at. * @param value The iconChunkIds to set. */ @@ -260,7 +277,7 @@ public object SnapshotKt { public operator fun com.google.protobuf.kotlin.DslList.set(index: kotlin.Int, value: com.google.protobuf.ByteString) { _builder.setIconChunkIds(index, value) }/** - * repeated bytes iconChunkIds = 9; + * repeated bytes iconChunkIds = 10; */ @kotlin.jvm.JvmSynthetic @kotlin.jvm.JvmName("clearIconChunkIds") @@ -274,7 +291,7 @@ public object SnapshotKt { @kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class) public class BlobsProxy private constructor() : com.google.protobuf.kotlin.DslProxy() /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 10; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 11; */ public val blobs: com.google.protobuf.kotlin.DslMap @kotlin.jvm.JvmSynthetic @@ -283,7 +300,7 @@ public object SnapshotKt { _builder.getBlobsMap() ) /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 10; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 11; */ @JvmName("putBlobs") public fun com.google.protobuf.kotlin.DslMap @@ -291,7 +308,7 @@ public object SnapshotKt { _builder.putBlobs(key, value) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 10; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 11; */ @kotlin.jvm.JvmSynthetic @JvmName("setBlobs") @@ -301,7 +318,7 @@ public object SnapshotKt { put(key, value) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 10; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 11; */ @kotlin.jvm.JvmSynthetic @JvmName("removeBlobs") @@ -310,7 +327,7 @@ public object SnapshotKt { _builder.removeBlobs(key) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 10; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 11; */ @kotlin.jvm.JvmSynthetic @JvmName("putAllBlobs") @@ -319,7 +336,7 @@ public object SnapshotKt { _builder.putAllBlobs(map) } /** - * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 10; + * map<string, .com.stevesoltys.seedvault.proto.Snapshot.Blob> blobs = 11; */ @kotlin.jvm.JvmSynthetic @JvmName("clearBlobs") @@ -365,24 +382,7 @@ public object SnapshotKt { } /** - * string state = 2; - */ - public var state: kotlin.String - @JvmName("getState") - get() = _builder.getState() - @JvmName("setState") - set(value) { - _builder.setState(value) - } - /** - * string state = 2; - */ - public fun clearState() { - _builder.clearState() - } - - /** - * .com.stevesoltys.seedvault.proto.Snapshot.BackupType type = 3; + * .com.stevesoltys.seedvault.proto.Snapshot.BackupType type = 2; */ public var type: com.stevesoltys.seedvault.proto.Snapshot.BackupType @JvmName("getType") @@ -392,14 +392,14 @@ public object SnapshotKt { _builder.setType(value) } /** - * .com.stevesoltys.seedvault.proto.Snapshot.BackupType type = 3; + * .com.stevesoltys.seedvault.proto.Snapshot.BackupType type = 2; */ public fun clearType() { _builder.clearType() } /** - * string name = 4; + * string name = 3; */ public var name: kotlin.String @JvmName("getName") @@ -409,14 +409,14 @@ public object SnapshotKt { _builder.setName(value) } /** - * string name = 4; + * string name = 3; */ public fun clearName() { _builder.clearName() } /** - * bool system = 5; + * bool system = 4; */ public var system: kotlin.Boolean @JvmName("getSystem") @@ -426,14 +426,14 @@ public object SnapshotKt { _builder.setSystem(value) } /** - * bool system = 5; + * bool system = 4; */ public fun clearSystem() { _builder.clearSystem() } /** - * bool launchableSystemApp = 6; + * bool launchableSystemApp = 5; */ public var launchableSystemApp: kotlin.Boolean @JvmName("getLaunchableSystemApp") @@ -443,7 +443,7 @@ public object SnapshotKt { _builder.setLaunchableSystemApp(value) } /** - * bool launchableSystemApp = 6; + * bool launchableSystemApp = 5; */ public fun clearLaunchableSystemApp() { _builder.clearLaunchableSystemApp() @@ -456,7 +456,7 @@ public object SnapshotKt { @kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class) public class ChunkIdsProxy private constructor() : com.google.protobuf.kotlin.DslProxy() /** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; */ public val chunkIds: com.google.protobuf.kotlin.DslList @kotlin.jvm.JvmSynthetic @@ -464,7 +464,7 @@ public object SnapshotKt { _builder.getChunkIdsList() ) /** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; * @param value The chunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -472,7 +472,7 @@ public object SnapshotKt { public fun com.google.protobuf.kotlin.DslList.add(value: com.google.protobuf.ByteString) { _builder.addChunkIds(value) }/** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; * @param value The chunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -481,7 +481,7 @@ public object SnapshotKt { public inline operator fun com.google.protobuf.kotlin.DslList.plusAssign(value: com.google.protobuf.ByteString) { add(value) }/** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; * @param values The chunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -489,7 +489,7 @@ public object SnapshotKt { public fun com.google.protobuf.kotlin.DslList.addAll(values: kotlin.collections.Iterable) { _builder.addAllChunkIds(values) }/** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; * @param values The chunkIds to add. */ @kotlin.jvm.JvmSynthetic @@ -498,7 +498,7 @@ public object SnapshotKt { public inline operator fun com.google.protobuf.kotlin.DslList.plusAssign(values: kotlin.collections.Iterable) { addAll(values) }/** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; * @param index The index to set the value at. * @param value The chunkIds to set. */ @@ -507,7 +507,7 @@ public object SnapshotKt { public operator fun com.google.protobuf.kotlin.DslList.set(index: kotlin.Int, value: com.google.protobuf.ByteString) { _builder.setChunkIds(index, value) }/** - * repeated bytes chunkIds = 7; + * repeated bytes chunkIds = 6; */ @kotlin.jvm.JvmSynthetic @kotlin.jvm.JvmName("clearChunkIds") @@ -515,7 +515,7 @@ public object SnapshotKt { _builder.clearChunkIds() } /** - * .com.stevesoltys.seedvault.proto.Snapshot.Apk apk = 8; + * .com.stevesoltys.seedvault.proto.Snapshot.Apk apk = 7; */ public var apk: com.stevesoltys.seedvault.proto.Snapshot.Apk @JvmName("getApk") @@ -525,13 +525,13 @@ public object SnapshotKt { _builder.setApk(value) } /** - * .com.stevesoltys.seedvault.proto.Snapshot.Apk apk = 8; + * .com.stevesoltys.seedvault.proto.Snapshot.Apk apk = 7; */ public fun clearApk() { _builder.clearApk() } /** - * .com.stevesoltys.seedvault.proto.Snapshot.Apk apk = 8; + * .com.stevesoltys.seedvault.proto.Snapshot.Apk apk = 7; * @return Whether the apk field is set. */ public fun hasApk(): kotlin.Boolean { diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt index 0a40f2ad..d1362e1d 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt @@ -39,7 +39,7 @@ data class BackupMetadata( time = s.token, androidVersion = s.sdkInt, androidIncremental = s.androidIncremental, - deviceName = s.name, + deviceName = "${s.name} - ${s.user}", d2dBackup = s.d2D, packageMetadataMap = s.appsMap.mapValues { (_, app) -> PackageMetadata.fromSnapshot(app) @@ -121,7 +121,6 @@ data class PackageMetadata( companion object { fun fromSnapshot(app: Snapshot.App) = PackageMetadata( time = app.time, - state = if (app.state.isBlank()) UNKNOWN_ERROR else PackageState.valueOf(app.state), backupType = when (app.type) { Snapshot.BackupType.FULL -> BackupType.FULL Snapshot.BackupType.KV -> BackupType.KV diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt index cc9655a6..bab7ca72 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -13,8 +13,6 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA -import com.stevesoltys.seedvault.transport.backup.PackageService -import com.stevesoltys.seedvault.transport.backup.isSystemApp import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream @@ -31,7 +29,6 @@ internal class MetadataManager( private val clock: Clock, private val metadataWriter: MetadataWriter, private val metadataReader: MetadataReader, - private val packageService: PackageService, ) { private val uninitializedMetadata = BackupMetadata(token = -42L, salt = "foo bar") @@ -52,10 +49,6 @@ internal class MetadataManager( return field } - private val launchableSystemApps by lazy { - packageService.launchableSystemApps.map { it.activityInfo.packageName }.toSet() - } - /** * Call this after a package has been backed up successfully. * @@ -75,15 +68,11 @@ internal class MetadataManager( modifyCachedMetadata { val now = clock.time() metadata.packageMetadataMap.getOrPut(packageName) { - val isSystemApp = packageInfo.isSystemApp() PackageMetadata( time = now, state = APK_AND_DATA, backupType = type, size = size, - system = isSystemApp, - isLaunchableSystemApp = isSystemApp && - launchableSystemApps.contains(packageName), ) }.apply { time = now @@ -111,15 +100,11 @@ internal class MetadataManager( check(packageState != APK_AND_DATA) { "Backup Error with non-error package state." } modifyCachedMetadata { metadata.packageMetadataMap.getOrPut(packageInfo.packageName) { - val isSystemApp = packageInfo.isSystemApp() PackageMetadata( time = 0L, state = packageState, backupType = backupType, name = packageInfo.applicationInfo?.loadLabel(context.packageManager), - system = isSystemApp, - isLaunchableSystemApp = isSystemApp && - launchableSystemApps.contains(packageInfo.packageName), ) }.state = packageState } @@ -137,14 +122,10 @@ internal class MetadataManager( packageState: PackageState, ) = modifyCachedMetadata { metadata.packageMetadataMap.getOrPut(packageInfo.packageName) { - val isSystemApp = packageInfo.isSystemApp() PackageMetadata( time = 0L, state = packageState, name = packageInfo.applicationInfo?.loadLabel(context.packageManager), - system = isSystemApp, - isLaunchableSystemApp = isSystemApp && - launchableSystemApps.contains(packageInfo.packageName), ) }.apply { state = packageState diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt index 003fff7e..b3473794 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataModule.kt @@ -9,7 +9,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val metadataModule = module { - single { MetadataManager(androidContext(), get(), get(), get(), get()) } + single { MetadataManager(androidContext(), get(), get(), get()) } single { MetadataWriterImpl() } single { MetadataReaderImpl(get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt index 1cd5a0c9..be8c6f21 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreator.kt @@ -5,6 +5,7 @@ package com.stevesoltys.seedvault.transport.backup +import android.Manifest import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageInfo @@ -18,7 +19,6 @@ import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.MetadataManager -import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.proto.Snapshot import com.stevesoltys.seedvault.proto.Snapshot.Apk import com.stevesoltys.seedvault.proto.Snapshot.App @@ -28,6 +28,9 @@ import io.github.oshai.kotlinlogging.KotlinLogging import org.calyxos.seedvault.core.backends.AppBackupFileType import org.calyxos.seedvault.core.toHexString +/** + * Creates a new [SnapshotCreator], because one is only valid for a single backup run. + */ internal class SnapshotCreatorFactory( private val context: Context, private val clock: Clock, @@ -39,6 +42,10 @@ internal class SnapshotCreatorFactory( SnapshotCreator(context, clock, packageService, settingsManager, metadataManager) } +/** + * Assembles snapshot information over the course of a single backup run + * and creates a [Snapshot] object in the end by calling [finalizeSnapshot]. + */ internal class SnapshotCreator( private val context: Context, private val clock: Clock, @@ -48,14 +55,21 @@ internal class SnapshotCreator( ) { private val log = KotlinLogging.logger { } + private val snapshotBuilder = Snapshot.newBuilder() private val appBuilderMap = mutableMapOf() private val blobsMap = mutableMapOf() private val launchableSystemApps by lazy { + // as we can't ask [PackageInfo] for this, we keep a set of packages around packageService.launchableSystemApps.map { it.activityInfo.packageName }.toSet() } + /** + * Call this after all blobs for the given [apk] have been saved to the backend. + * The [apk] must contain the ordered list of chunk IDs + * and the given [blobMap] must have one [Blob] per chunk ID. + */ fun onApkBackedUp( packageInfo: PackageInfo, apk: Apk, @@ -71,6 +85,14 @@ internal class SnapshotCreator( blobsMap.putAll(blobMap) } + /** + * Call this after all blobs for the package identified by the given [packageInfo] + * have been saved to the backend. + * The given [backupData] must contain the full ordered list of [BackupData.chunkIds] + * and the [BackupData.blobMap] must have one [Blob] per chunk ID. + * + * Failure to call this method results in the package effectively not getting backed up. + */ fun onPackageBackedUp( packageInfo: PackageInfo, backupType: BackupType, @@ -83,7 +105,6 @@ internal class SnapshotCreator( App.newBuilder() }.apply { time = clock.time() - state = APK_AND_DATA.name // TODO review those states and their usefulness for snapshot type = backupType.forSnapshot() val label = packageInfo.applicationInfo?.loadLabel(context.packageManager) if (label != null) name = label.toString() @@ -95,26 +116,31 @@ internal class SnapshotCreator( metadataManager.onPackageBackedUp(packageInfo, backupType, backupData.size) } + /** + * Call this after all blobs for the app icons have been saved to the backend. + */ fun onIconsBackedUp(backupData: BackupData) { snapshotBuilder.addAllIconChunkIds(backupData.chunkIds.forProto()) blobsMap.putAll(backupData.blobMap) } + /** + * Must get called after all backup data was saved to the backend. + * Returns the assembled [Snapshot] which must be saved to the backend as well + * to complete the current backup run. + * + * Internal state will be cleared to free up memory. + * Still, it isn't safe to re-use an instance of this class, after it has been finalized. + */ fun finalizeSnapshot(): Snapshot { log.info { "finalizeSnapshot()" } - val userName = getUserName() - val deviceName = if (userName == null) { - "${Build.MANUFACTURER} ${Build.MODEL}" - } else { - "${Build.MANUFACTURER} ${Build.MODEL} - $userName" - } - @SuppressLint("HardwareIds") val snapshot = snapshotBuilder.apply { version = VERSION.toInt() token = clock.time() - name = deviceName - androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID) + name = "${Build.MANUFACTURER} ${Build.MODEL}" + user = getUserName() ?: "" + androidId = Settings.Secure.getString(context.contentResolver, ANDROID_ID) ?: "" sdkInt = Build.VERSION.SDK_INT androidIncremental = Build.VERSION.INCREMENTAL d2D = settingsManager.d2dBackupsEnabled() @@ -123,11 +149,13 @@ internal class SnapshotCreator( }.build() appBuilderMap.clear() snapshotBuilder.clear() + blobsMap.clear() return snapshot } private fun getUserName(): String? { - val perm = "android.permission.QUERY_USERS" + @Suppress("UNRESOLVED_REFERENCE") // hidden AOSP API + val perm = Manifest.permission.QUERY_USERS return if (context.checkSelfPermission(perm) == PERMISSION_GRANTED) { val userManager = context.getSystemService(UserManager::class.java) ?: return null userManager.userName diff --git a/app/src/main/proto/snapshot.proto b/app/src/main/proto/snapshot.proto index e5e20823..142a3ea7 100644 --- a/app/src/main/proto/snapshot.proto +++ b/app/src/main/proto/snapshot.proto @@ -8,23 +8,23 @@ message Snapshot { uint32 version = 1; uint64 token = 2; string name = 3; - string androidId = 4; - uint32 sdkInt = 5; - string androidIncremental = 6; - bool d2d = 7; - map apps = 8; - repeated bytes iconChunkIds = 9; - map blobs = 10; + string user = 4; + string androidId = 5; + uint32 sdkInt = 6; + string androidIncremental = 7; + bool d2d = 8; + map apps = 9; + repeated bytes iconChunkIds = 10; + map blobs = 11; message App { uint64 time = 1; - string state = 2; - BackupType type = 3; - string name = 4; - bool system = 5; - bool launchableSystemApp = 6; - repeated bytes chunkIds = 7; - Apk apk = 8; + BackupType type = 2; + string name = 3; + bool system = 4; + bool launchableSystemApp = 5; + repeated bytes chunkIds = 6; + Apk apk = 7; } enum BackupType { diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt index 368777ca..974a9bce 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -9,7 +9,6 @@ import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP -import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.PackageInfo import android.content.pm.PackageManager import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -23,7 +22,6 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED import com.stevesoltys.seedvault.settings.SettingsManager -import com.stevesoltys.seedvault.transport.backup.PackageService import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -37,7 +35,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.koin.core.context.stopKoin import org.robolectric.annotation.Config -import java.io.ByteArrayOutputStream import java.io.FileInputStream import java.io.FileOutputStream import kotlin.random.Random @@ -53,7 +50,6 @@ class MetadataManagerTest { private val clock: Clock = mockk() private val metadataWriter: MetadataWriter = mockk() private val metadataReader: MetadataReader = mockk() - private val packageService: PackageService = mockk() private val settingsManager: SettingsManager = mockk() private val manager = MetadataManager( @@ -61,7 +57,6 @@ class MetadataManagerTest { clock = clock, metadataWriter = metadataWriter, metadataReader = metadataReader, - packageService = packageService, ) private val packageManager: PackageManager = mockk() @@ -76,7 +71,6 @@ class MetadataManagerTest { private val saltBytes = Random.nextBytes(METADATA_SALT_SIZE) private val salt = saltBytes.encodeBase64() private val initialMetadata = BackupMetadata(token = token, salt = salt) - private val storageOutputStream = ByteArrayOutputStream() private val cacheOutputStream: FileOutputStream = mockk() private val cacheInputStream: FileInputStream = mockk() private val encodedMetadata = getRandomByteArray() @@ -93,7 +87,6 @@ class MetadataManagerTest { @Test fun `test onPackageBackedUp()`() { - packageInfo.applicationInfo!!.flags = FLAG_SYSTEM val updatedMetadata = initialMetadata.copy( time = time, packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced @@ -103,7 +96,6 @@ class MetadataManagerTest { updatedMetadata.packageMetadataMap[packageName] = packageMetadata every { context.packageManager } returns packageManager - every { packageService.launchableSystemApps } returns emptyList() expectReadFromCache() every { clock.time() } returns time expectWriteToCache(initialMetadata) @@ -115,8 +107,6 @@ class MetadataManagerTest { state = APK_AND_DATA, backupType = BackupType.FULL, size = size, - system = true, - isLaunchableSystemApp = false, ), manager.getPackageMetadata(packageName) ) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreatorTest.kt new file mode 100644 index 00000000..720cd522 --- /dev/null +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/SnapshotCreatorTest.kt @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2024 The Calyx Institute + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.stevesoltys.seedvault.transport.backup + +import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.ApplicationInfo.FLAG_SYSTEM +import android.content.pm.ResolveInfo +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.stevesoltys.seedvault.TestApp +import com.stevesoltys.seedvault.header.VERSION +import com.stevesoltys.seedvault.metadata.BackupType +import com.stevesoltys.seedvault.proto.Snapshot +import com.stevesoltys.seedvault.transport.TransportTest +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import kotlin.random.Random + +@RunWith(AndroidJUnit4::class) +@Config( + sdk = [34], // TODO: Drop once robolectric supports 35 + application = TestApp::class +) +internal class SnapshotCreatorTest : TransportTest() { + + private val ctx: Context = ApplicationProvider.getApplicationContext() + private val packageService: PackageService = mockk() + private val snapshotCreator = + SnapshotCreator(ctx, clock, packageService, settingsManager, metadataManager) + + @Test + fun `test onApkBackedUp`() { + every { applicationInfo.loadLabel(any()) } returns name + every { clock.time() } returns token + every { settingsManager.d2dBackupsEnabled() } returns Random.nextBoolean() + + snapshotCreator.onApkBackedUp(packageInfo, apk, blobMap) + val s = snapshotCreator.finalizeSnapshot() + + assertEquals(apk, s.appsMap[packageName]?.apk) + assertEquals(name, s.appsMap[packageName]?.name) + assertEquals(blobMap, s.blobsMap) + } + + @Test + fun `test onPackageBackedUp`() { + val size = apkBackupData.size + val isSystem = Random.nextBoolean() + val appInfo = mockk { + flags = if (isSystem) FLAG_SYSTEM else 0 + } + packageInfo.applicationInfo = appInfo + val resolveInfo = ResolveInfo().apply { // if isSystem, then it will be launchable + activityInfo = ActivityInfo().apply { + packageName = this@SnapshotCreatorTest.packageName + } + } + every { appInfo.loadLabel(any()) } returns name + every { metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, size) } just Runs + every { clock.time() } returns token andThen token + 1 + every { settingsManager.d2dBackupsEnabled() } returns Random.nextBoolean() + every { packageService.launchableSystemApps } returns listOf(resolveInfo) + + snapshotCreator.onPackageBackedUp(packageInfo, BackupType.FULL, apkBackupData) + val s = snapshotCreator.finalizeSnapshot() + + assertEquals(name, s.appsMap[packageName]?.name) + assertEquals(token, s.appsMap[packageName]?.time) + assertEquals(Snapshot.BackupType.FULL, s.appsMap[packageName]?.type) + assertEquals(isSystem, s.appsMap[packageName]?.system) + assertEquals(isSystem, s.appsMap[packageName]?.launchableSystemApp) + assertEquals(apkBackupData.chunkIds.forProto(), s.appsMap[packageName]?.chunkIdsList) + assertEquals(apkBackupData.blobMap, s.blobsMap) + } + + @Test + fun `test onPackageBackedUp handles no application info`() { + packageInfo.applicationInfo = null + + val size = apkBackupData.size + every { metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, size) } just Runs + every { clock.time() } returns token andThen token + 1 + every { settingsManager.d2dBackupsEnabled() } returns Random.nextBoolean() + every { packageService.launchableSystemApps } returns emptyList() + + snapshotCreator.onPackageBackedUp(packageInfo, BackupType.FULL, apkBackupData) + snapshotCreator.finalizeSnapshot() + } + + @Test + fun `test onIconsBackedUp`() { + every { clock.time() } returns token andThen token + 1 + every { settingsManager.d2dBackupsEnabled() } returns Random.nextBoolean() + + snapshotCreator.onIconsBackedUp(apkBackupData) + val s = snapshotCreator.finalizeSnapshot() + + assertEquals(apkBackupData.chunkIds.forProto(), s.iconChunkIdsList) + assertEquals(apkBackupData.blobMap, s.blobsMap) + } + + @Test + fun `test finalize`() { + val d2d = Random.nextBoolean() + every { clock.time() } returns token + every { settingsManager.d2dBackupsEnabled() } returns d2d + + val s = snapshotCreator.finalizeSnapshot() + + assertEquals(VERSION, s.version.toByte()) + assertEquals(token, s.token) + assertEquals("robolectric robolectric", s.name) + assertEquals("", s.user) // no perm + assertEquals("", s.androidId) // not mocked + assertEquals(34, s.sdkInt) // as per config above, needs bump once possible + assertEquals("unknown", s.androidIncremental) + assertEquals(d2d, s.d2D) + assertEquals(0, s.appsCount) + assertEquals(0, s.iconChunkIdsCount) + assertEquals(emptyMap(), s.blobsMap) + } + +}