Clean up metadata as it lost most of its importance

Historically, metadata was uploaded to the backend after each app update and contained all essential data that is now in snapshots. We still support reading metadata for legacy backups and use the metadata classes as a common wrapper for snapshots. However, there is no need anymore to write out complete historic metadata and maintain duplicated unused information there. This got removed. THe information we do still save and write out is only for UI representation of backup state.

The time of last backup is now managed by SettingsManager.
This commit is contained in:
Torsten Grote 2024-09-13 12:07:34 -03:00
parent a268116e06
commit 237fd683bd
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
21 changed files with 82 additions and 793 deletions

View file

@ -85,7 +85,6 @@ internal interface LargeTestBase : KoinComponent {
fun resetApplicationState() {
backupManager.setAutoRestore(false)
settingsManager.token = null
val sharedPreferences = permitDiskReads {
PreferenceManager.getDefaultSharedPreferences(targetContext)

View file

@ -20,7 +20,6 @@ import android.os.Handler
import android.os.Looper
import android.provider.DocumentsContract
import android.util.Log
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.FlashDrive
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.storage.AUTHORITY_STORAGE
@ -34,7 +33,6 @@ class UsbIntentReceiver : UsbMonitor() {
// using KoinComponent would crash robolectric tests :(
private val settingsManager: SettingsManager by lazy { get().get() }
private val metadataManager: MetadataManager by lazy { get().get() }
private val backupManager: IBackupManager by lazy { get().get() }
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
@ -44,14 +42,15 @@ class UsbIntentReceiver : UsbMonitor() {
val attachedFlashDrive = FlashDrive.from(device)
return if (savedFlashDrive == attachedFlashDrive) {
Log.d(TAG, "Matches stored device, checking backup time...")
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
val lastBackupTime = settingsManager.lastBackupTime.value ?: 0
val backupMillis = System.currentTimeMillis() - lastBackupTime
if (backupMillis >= settingsManager.backupFrequencyInMillis) {
Log.d(TAG, "Last backup older than it should be, requesting a backup...")
Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}")
Log.d(TAG, " ${Date(lastBackupTime)}")
true
} else {
Log.d(TAG, "We have a recent backup, not requesting a new one.")
Log.d(TAG, " ${Date(metadataManager.getLastBackupTime())}")
Log.d(TAG, " ${Date(lastBackupTime)}")
false
}
} else {

View file

@ -11,13 +11,8 @@ import android.content.pm.PackageInfo
import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.distinctUntilChanged
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.isSystemApp
import java.io.FileNotFoundException
@ -37,7 +32,6 @@ internal class MetadataManager(
private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader,
private val packageService: PackageService,
private val settingsManager: SettingsManager,
) {
private val uninitializedMetadata = BackupMetadata(token = -42L, salt = "foo bar")
@ -54,7 +48,6 @@ internal class MetadataManager(
// This should cause requiresInit() return true
uninitializedMetadata.copy(version = (-1).toByte())
}
mLastBackupTime.postValue(field.time)
}
return field
}
@ -63,40 +56,6 @@ internal class MetadataManager(
packageService.launchableSystemApps.map { it.activityInfo.packageName }.toSet()
}
/**
* Call this after a package's APK has been backed up successfully.
*
* It updates the packages' metadata to the internal cache.
*/
@Synchronized
@Throws(IOException::class)
fun onApkBackedUp(
packageInfo: PackageInfo,
packageMetadata: PackageMetadata,
) {
val packageName = packageInfo.packageName
metadata.packageMetadataMap[packageName]?.let {
check(packageMetadata.version != null) {
"APK backup returned version null"
}
}
val oldPackageMetadata = metadata.packageMetadataMap[packageName]
?: PackageMetadata()
modifyCachedMetadata {
val isSystemApp = packageInfo.isSystemApp()
metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy(
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp,
isLaunchableSystemApp = isSystemApp && launchableSystemApps.contains(packageName),
version = packageMetadata.version,
installer = packageMetadata.installer,
splits = packageMetadata.splits,
sha256 = packageMetadata.sha256,
signatures = packageMetadata.signatures
)
}
}
/**
* Call this after a package has been backed up successfully.
*
@ -115,8 +74,6 @@ internal class MetadataManager(
val packageName = packageInfo.packageName
modifyCachedMetadata {
val now = clock.time()
metadata.time = now
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
metadata.packageMetadataMap.getOrPut(packageName) {
val isSystemApp = packageInfo.isSystemApp()
PackageMetadata(
@ -124,7 +81,6 @@ internal class MetadataManager(
state = APK_AND_DATA,
backupType = type,
size = size,
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp,
isLaunchableSystemApp = isSystemApp &&
launchableSystemApps.contains(packageName),
@ -135,10 +91,6 @@ internal class MetadataManager(
backupType = type
// don't override a previous K/V size, if there were no K/V changes
if (size != null) this.size = size
// update name, if none was set, yet (can happen while migrating to storing names)
if (this.name == null) {
this.name = packageInfo.applicationInfo?.loadLabel(context.packageManager)
}
}
}
}
@ -203,9 +155,15 @@ internal class MetadataManager(
}
}
@Synchronized
fun getPackageMetadata(packageName: String): PackageMetadata? {
return metadata.packageMetadataMap[packageName]?.copy()
}
@Throws(IOException::class)
private fun modifyCachedMetadata(modFun: () -> Unit) {
val oldMetadata = metadata.copy( // copy map, otherwise it will re-use same reference
val oldMetadata = metadata.copy(
// copy map, otherwise it will re-use same reference
packageMetadataMap = PackageMetadataMap(metadata.packageMetadataMap),
)
try {
@ -217,34 +175,6 @@ internal class MetadataManager(
metadata = oldMetadata
throw IOException(e)
}
mLastBackupTime.postValue(metadata.time) // TODO only do after snapshot was written
}
/**
* Returns the last backup time in unix epoch milli seconds.
*
* Note that this might be a blocking I/O call.
*/
@Synchronized
fun getLastBackupTime(): Long = mLastBackupTime.value ?: metadata.time
private val mLastBackupTime = MutableLiveData<Long>()
internal val lastBackupTime: LiveData<Long> = mLastBackupTime.distinctUntilChanged()
internal val salt: String
@Synchronized get() = metadata.salt
internal val requiresInit: Boolean
@Synchronized get() = metadata == uninitializedMetadata || metadata.version < VERSION
@Synchronized
fun getPackageMetadata(packageName: String): PackageMetadata? {
return metadata.packageMetadataMap[packageName]?.copy()
}
@Synchronized
fun getPackagesBackupSize(): Long {
return metadata.packageMetadataMap.values.sumOf { it.size ?: 0L }
}
@Synchronized

View file

@ -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(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) }
single { MetadataManager(androidContext(), get(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl() }
single<MetadataReader> { MetadataReaderImpl(get()) }
}

View file

@ -94,14 +94,14 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
val json = JSONObject(bytes.toString(Utf8))
// get backup metadata and check expectations
val meta = json.getJSONObject(JSON_METADATA)
val version = meta.getInt(JSON_METADATA_VERSION).toByte()
val version = meta.optInt(JSON_METADATA_VERSION, VERSION.toInt()).toByte()
if (expectedVersion != null && version != expectedVersion) {
throw SecurityException(
"Invalid version '${version.toInt()}' in metadata," +
"expected '${expectedVersion.toInt()}'."
)
}
val token = meta.getLong(JSON_METADATA_TOKEN)
val token = meta.optLong(JSON_METADATA_TOKEN, 0)
if (expectedToken != null && token != expectedToken) throw SecurityException(
"Invalid token '$token' in metadata, expected '$expectedToken'."
)
@ -157,11 +157,11 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
return BackupMetadata(
version = version,
token = token,
salt = if (version == 0.toByte()) "" else meta.getString(JSON_METADATA_SALT),
time = meta.getLong(JSON_METADATA_TIME),
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
deviceName = meta.getString(JSON_METADATA_NAME),
salt = if (version == 0.toByte()) "" else meta.optString(JSON_METADATA_SALT, ""),
time = meta.optLong(JSON_METADATA_TIME, -1),
androidVersion = meta.optInt(JSON_METADATA_SDK_INT, 0),
androidIncremental = meta.optString(JSON_METADATA_INCREMENTAL),
deviceName = meta.optString(JSON_METADATA_NAME),
d2dBackup = meta.optBoolean(JSON_METADATA_D2D_BACKUP, false),
packageMetadataMap = packageMetadataMap,
)

View file

@ -6,42 +6,18 @@
package com.stevesoltys.seedvault.metadata
import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.io.OutputStream
interface MetadataWriter {
@Throws(IOException::class)
fun write(metadata: BackupMetadata, outputStream: OutputStream)
fun encode(metadata: BackupMetadata): ByteArray
}
internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
@Throws(IOException::class)
override fun write(metadata: BackupMetadata, outputStream: OutputStream) {
outputStream.write(ByteArray(1).apply { this[0] = metadata.version })
crypto.newEncryptingStreamV1(outputStream, getAD(metadata.version, metadata.token)).use {
it.write(encode(metadata))
}
}
internal class MetadataWriterImpl : MetadataWriter {
override fun encode(metadata: BackupMetadata): ByteArray {
val json = JSONObject().apply {
put(JSON_METADATA, JSONObject().apply {
put(JSON_METADATA_VERSION, metadata.version.toInt())
put(JSON_METADATA_TOKEN, metadata.token)
put(JSON_METADATA_SALT, metadata.salt)
put(JSON_METADATA_TIME, metadata.time)
put(JSON_METADATA_SDK_INT, metadata.androidVersion)
put(JSON_METADATA_INCREMENTAL, metadata.androidIncremental)
put(JSON_METADATA_NAME, metadata.deviceName)
put(JSON_METADATA_D2D_BACKUP, metadata.d2dBackup)
})
put(JSON_METADATA, JSONObject())
}
for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
json.put(packageName, JSONObject().apply {
@ -57,31 +33,14 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
if (packageMetadata.size != null) {
put(JSON_PACKAGE_SIZE, packageMetadata.size)
}
if (packageMetadata.name != null) {
put(JSON_PACKAGE_APP_NAME, packageMetadata.name)
}
if (packageMetadata.system) {
put(JSON_PACKAGE_SYSTEM, true)
}
if (packageMetadata.isLaunchableSystemApp) {
put(JSON_PACKAGE_SYSTEM_LAUNCHER, true)
}
packageMetadata.version?.let { put(JSON_PACKAGE_VERSION, it) }
packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) }
packageMetadata.splits?.let { splits ->
put(JSON_PACKAGE_SPLITS, JSONArray().apply {
for (split in splits) put(JSONObject().apply {
put(JSON_PACKAGE_SPLIT_NAME, split.name)
if (split.size != null) put(JSON_PACKAGE_SIZE, split.size)
put(JSON_PACKAGE_SHA256, split.sha256)
})
})
}
packageMetadata.sha256?.let { put(JSON_PACKAGE_SHA256, it) }
packageMetadata.signatures?.let { put(JSON_PACKAGE_SIGNATURES, JSONArray(it)) }
})
}
return json.toString().toByteArray(Utf8)
}
}

View file

@ -10,6 +10,8 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.hardware.usb.UsbDevice
import android.net.Uri
import androidx.annotation.UiThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.preference.PreferenceManager
import com.stevesoltys.seedvault.backend.webdav.WebDavHandler.Companion.createWebDavProperties
import com.stevesoltys.seedvault.permitDiskReads
@ -55,20 +57,19 @@ private const val PREF_KEY_BACKUP_APP_BLACKLIST = "backupAppBlacklist"
private const val PREF_KEY_BACKUP_STORAGE = "backup_storage"
internal const val PREF_KEY_UNLIMITED_QUOTA = "unlimited_quota"
internal const val PREF_KEY_D2D_BACKUPS = "d2d_backups"
internal const val PREF_KEY_LAST_BACKUP = "lastBackup"
class SettingsManager(private val context: Context) {
private val prefs = permitDiskReads {
PreferenceManager.getDefaultSharedPreferences(context)
}
private val mLastBackupTime = MutableLiveData(prefs.getLong(PREF_KEY_LAST_BACKUP, -1))
fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}
fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
/**
* Returns a LiveData of the last backup time in unix epoch milli seconds.
*/
internal val lastBackupTime: LiveData<Long> = mLastBackupTime
/**
* This gets accessed by non-UI threads when saving with [PreferenceManager]
@ -81,7 +82,7 @@ class SettingsManager(private val context: Context) {
@Volatile
var token: Long? = null
set(newToken) {
private set(newToken) {
if (newToken == null) {
prefs.edit()
.remove(PREF_KEY_TOKEN)
@ -121,6 +122,21 @@ class SettingsManager(private val context: Context) {
}
}
fun registerOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}
fun unregisterOnSharedPreferenceChangeListener(listener: OnSharedPreferenceChangeListener) {
prefs.unregisterOnSharedPreferenceChangeListener(listener)
}
fun onSuccessfulBackupCompleted(token: Long) {
this.token = token
val now = System.currentTimeMillis()
prefs.edit().putLong(PREF_KEY_LAST_BACKUP, now).apply()
mLastBackupTime.postValue(now)
}
fun setStorageBackend(plugin: Backend) {
val value = when (plugin) {
is SafBackend -> StoragePluginType.SAF

View file

@ -88,7 +88,7 @@ internal class SettingsViewModel(
private val mBackupPossible = MutableLiveData(false)
val backupPossible: LiveData<Boolean> = mBackupPossible
internal val lastBackupTime = metadataManager.lastBackupTime
internal val lastBackupTime = settingsManager.lastBackupTime
internal val appBackupWorkInfo =
workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map {
it.getOrNull(0)
@ -143,8 +143,6 @@ internal class SettingsViewModel(
initialValue = false,
)
scope.launch {
// ensures the lastBackupTime LiveData gets set
metadataManager.getLastBackupTime()
// update running state
isBackupRunning.collect {
onBackupRunningStateChanged()
@ -258,21 +256,6 @@ internal class SettingsViewModel(
fun onBackupEnabled(enabled: Boolean) {
if (enabled) {
if (metadataManager.requiresInit) {
val onError: () -> Unit = {
viewModelScope.launch(Dispatchers.Main) {
val res = R.string.storage_check_fragment_backup_error
Toast.makeText(app, res, LENGTH_LONG).show()
}
}
viewModelScope.launch(Dispatchers.IO) {
backupInitializer.initialize(onError) {
mInitEvent.postEvent(false)
scheduleAppBackup(CANCEL_AND_REENQUEUE)
}
mInitEvent.postEvent(true)
}
}
// enable call log backups for existing installs (added end of 2020)
enableCallLogBackup()
} else {

View file

@ -5,6 +5,7 @@
package com.stevesoltys.seedvault.transport.backup
import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.settings.SettingsManager
@ -29,6 +30,7 @@ internal class AppBackupManager(
var snapshotCreator: SnapshotCreator? = null
private set
@WorkerThread
suspend fun beforeBackup() {
log.info { "Loading existing snapshots and blobs..." }
val blobInfos = mutableListOf<FileInfo>()
@ -48,25 +50,26 @@ internal class AppBackupManager(
blobCache.populateCache(blobInfos, snapshots)
}
suspend fun afterBackupFinished(success: Boolean): Boolean {
@WorkerThread
suspend fun afterBackupFinished(success: Boolean): com.stevesoltys.seedvault.proto.Snapshot? {
log.info { "After backup finished. Success: $success" }
// free up memory by clearing blobs cache
blobCache.clear()
var result = false
try {
return try {
if (success) {
val snapshot =
snapshotCreator?.finalizeSnapshot() ?: error("Had no snapshotCreator")
keepTrying { // saving this is so important, we even keep trying
snapshotManager.saveSnapshot(snapshot)
}
settingsManager.token = snapshot.token
settingsManager.onSuccessfulBackupCompleted(snapshot.token)
// after snapshot was written, we can clear local cache as its info is in snapshot
blobCache.clearLocalCache()
}
result = true
snapshot
} else null
} catch (e: Exception) {
log.error(e) { "Error finishing backup" }
null
} finally {
snapshotCreator = null
}

View file

@ -199,15 +199,6 @@ internal class BackupCoordinator(
flags: Int,
): Int {
state.cancelReason = UNKNOWN_ERROR
if (metadataManager.requiresInit) {
Log.w(TAG, "Metadata requires re-init!")
// Tell the system that we are not initialized, it will initialize us afterwards.
// This will start a new restore set to upgrade from legacy format
// by starting a clean backup with all files using the new version.
//
// This causes a backup error, but things should go back to normal afterwards.
return TRANSPORT_NOT_INITIALIZED
}
return kv.performBackup(packageInfo, data, flags)
}
@ -324,8 +315,6 @@ internal class BackupCoordinator(
// tell K/V backup to finish
val backupData = kv.finishBackup()
snapshotCreator.onPackageBackedUp(packageInfo, BackupType.KV, backupData)
// TODO unify both calls
metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, backupData.size)
TRANSPORT_OK
} catch (e: Exception) {
Log.e(TAG, "Error finishing K/V backup for $packageName", e)
@ -345,8 +334,6 @@ internal class BackupCoordinator(
try {
val backupData = full.finishBackup()
snapshotCreator.onPackageBackedUp(packageInfo, BackupType.FULL, backupData)
// TODO unify both calls
metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, backupData.size)
TRANSPORT_OK
} catch (e: Exception) {
Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
@ -362,7 +349,6 @@ internal class BackupCoordinator(
else -> throw IllegalStateException("Unexpected state in finishBackup()")
}
// TODO is this only nice to have info, or do we need to do more?
private fun onPackageBackupError(packageInfo: PackageInfo, type: BackupType) {
val packageName = packageInfo.packageName
try {

View file

@ -19,7 +19,7 @@ val backupModule = module {
val snapshotFolder = File(androidContext().filesDir, "snapshots")
SnapshotManager(snapshotFolder, get(), get(), get())
}
single { SnapshotCreatorFactory(androidContext(), get(), get(), get()) }
single { SnapshotCreatorFactory(androidContext(), get(), get(), get(), get()) }
single { InputFactory() }
single {
PackageService(

View file

@ -17,6 +17,7 @@ import com.google.protobuf.ByteString
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
@ -32,8 +33,10 @@ internal class SnapshotCreatorFactory(
private val clock: Clock,
private val packageService: PackageService,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
) {
fun createSnapshotCreator() = SnapshotCreator(context, clock, packageService, settingsManager)
fun createSnapshotCreator() =
SnapshotCreator(context, clock, packageService, settingsManager, metadataManager)
}
internal class SnapshotCreator(
@ -41,6 +44,7 @@ internal class SnapshotCreator(
private val clock: Clock,
private val packageService: PackageService,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
) {
private val log = KotlinLogging.logger { }
@ -88,6 +92,7 @@ internal class SnapshotCreator(
addAllChunkIds(chunkIds)
}
blobsMap.putAll(backupData.chunkMap)
metadataManager.onPackageBackedUp(packageInfo, backupType, backupData.size)
}
fun onIconsBackedUp(backupData: BackupData) {

View file

@ -11,7 +11,7 @@ import android.text.format.DateUtils.getRelativeTimeSpanString
import com.stevesoltys.seedvault.R
fun Long.toRelativeTime(context: Context): CharSequence {
return if (this == 0L) {
return if (this == 0L || this == -1L) {
context.getString(R.string.settings_backup_last_backup_never)
} else {
val now = System.currentTimeMillis()

View file

@ -22,6 +22,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.AppBackupManager
import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.hexFromProto
import com.stevesoltys.seedvault.worker.BackupRequester
import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent
@ -138,18 +139,25 @@ internal class NotificationBackupObserver(
Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")
}
var success = status == 0
val size = if (success) metadataManager.getPackagesBackupSize() else 0L
val total = try {
packageService.allUserPackages.size
} catch (e: Exception) {
Log.e(TAG, "Error getting number of all user packages: ", e)
requestedPackages
}
runBlocking {
val snapshot = runBlocking {
check(!Looper.getMainLooper().isCurrentThread)
Log.d(TAG, "Finalizing backup...")
success = appBackupManager.afterBackupFinished(success)
val snapshot = appBackupManager.afterBackupFinished(success)
success = snapshot != null
snapshot
}
val size = if (snapshot != null) { // TODO count size of APKs separately
val chunkIds = snapshot.appsMap.values.flatMap { it.chunkIdsList }
chunkIds.sumOf {
snapshot.blobsMap[it.hexFromProto()]?.uncompressedLength?.toLong() ?: 0L
}
} else 0L
nm.onBackupFinished(success, numPackagesToReport, total, size)
}
}

View file

@ -7,25 +7,20 @@ package com.stevesoltys.seedvault.metadata
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.pm.ActivityInfo
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 android.content.pm.ResolveInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.TestApp
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService
@ -37,22 +32,16 @@ import io.mockk.verify
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.assertThrows
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.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import kotlin.random.Random
@Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class)
@Config(
sdk = [34], // TODO: Drop once robolectric supports 35
@ -62,7 +51,6 @@ class MetadataManagerTest {
private val context: Context = mockk()
private val clock: Clock = mockk()
private val crypto: Crypto = mockk()
private val metadataWriter: MetadataWriter = mockk()
private val metadataReader: MetadataReader = mockk()
private val packageService: PackageService = mockk()
@ -74,7 +62,6 @@ class MetadataManagerTest {
metadataWriter = metadataWriter,
metadataReader = metadataReader,
packageService = packageService,
settingsManager = settingsManager,
)
private val packageManager: PackageManager = mockk()
@ -104,187 +91,6 @@ class MetadataManagerTest {
stopKoin()
}
@Test
fun `test onApkBackedUp() with no prior package metadata`() {
val packageMetadata = PackageMetadata(
time = 0L,
version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(),
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectModifyMetadata(initialMetadata)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(packageMetadata, manager.getPackageMetadata(packageName))
verify {
cacheInputStream.close()
cacheOutputStream.close()
}
}
@Test
fun `test onApkBackedUp() sets system metadata`() {
packageInfo.applicationInfo = ApplicationInfo().apply { flags = FLAG_SYSTEM }
val packageMetadata = PackageMetadata(
time = 0L,
version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(),
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
every { packageService.launchableSystemApps } returns listOf(
ResolveInfo().apply {
activityInfo = ActivityInfo().apply {
packageName = this@MetadataManagerTest.packageName
}
}
)
expectReadFromCache()
expectModifyMetadata(initialMetadata)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(
packageMetadata.copy(system = true, isLaunchableSystemApp = true),
manager.getPackageMetadata(packageName),
)
verify {
cacheInputStream.close()
cacheOutputStream.close()
}
}
@Test
fun `test onApkBackedUp() with existing package metadata`() {
val packageMetadata = PackageMetadata(
time = time,
version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(),
signatures = listOf("sig")
)
initialMetadata.packageMetadataMap[packageName] = packageMetadata
val updatedPackageMetadata = PackageMetadata(
time = time,
version = packageMetadata.version!! + 1,
installer = getRandomString(),
signatures = listOf("sig foo")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectWriteToCache(initialMetadata)
manager.onApkBackedUp(packageInfo, updatedPackageMetadata)
assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName))
verify {
cacheInputStream.close()
cacheOutputStream.close()
}
}
@Test
fun `test onApkBackedUp() does not change package state`() {
var version = Random.nextLong(Long.MAX_VALUE)
var packageMetadata = PackageMetadata(
version = version,
installer = getRandomString(),
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
expectWriteToCache(initialMetadata)
val oldState = UNKNOWN_ERROR
// state doesn't change for APK_AND_DATA
packageMetadata = packageMetadata.copy(version = ++version, state = APK_AND_DATA)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(
packageMetadata.copy(state = oldState),
manager.getPackageMetadata(packageName)
)
// state doesn't change for QUOTA_EXCEEDED
packageMetadata = packageMetadata.copy(version = ++version, state = QUOTA_EXCEEDED)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(
packageMetadata.copy(state = oldState),
manager.getPackageMetadata(packageName)
)
// state doesn't change for NO_DATA
packageMetadata = packageMetadata.copy(version = ++version, state = NO_DATA)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(
packageMetadata.copy(state = oldState),
manager.getPackageMetadata(packageName)
)
// state doesn't change for NOT_ALLOWED
packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(
packageMetadata.copy(state = oldState),
manager.getPackageMetadata(packageName)
)
// state doesn't change for WAS_STOPPED
packageMetadata = packageMetadata.copy(version = ++version, state = WAS_STOPPED)
manager.onApkBackedUp(packageInfo, packageMetadata)
assertEquals(
packageMetadata.copy(state = oldState),
manager.getPackageMetadata(packageName)
)
verify {
cacheInputStream.close()
cacheOutputStream.close()
}
}
@Test
fun `test onApkBackedUp() throws while writing local cache`() {
val packageMetadata = PackageMetadata(
time = 0L,
version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(),
signatures = listOf("sig")
)
every { context.packageManager } returns packageManager
expectReadFromCache()
assertNull(manager.getPackageMetadata(packageName))
every { metadataWriter.encode(initialMetadata) } returns encodedMetadata
every {
context.openFileOutput(
METADATA_CACHE_FILE,
MODE_PRIVATE
)
} throws FileNotFoundException()
assertThrows<IOException> {
manager.onApkBackedUp(packageInfo, packageMetadata)
}
// metadata change got reverted
assertNull(manager.getPackageMetadata(packageName))
verify {
cacheInputStream.close()
}
}
@Test
fun `test onPackageBackedUp()`() {
packageInfo.applicationInfo!!.flags = FLAG_SYSTEM
@ -300,7 +106,7 @@ class MetadataManagerTest {
every { packageService.launchableSystemApps } returns emptyList()
expectReadFromCache()
every { clock.time() } returns time
expectModifyMetadata(initialMetadata)
expectWriteToCache(initialMetadata)
manager.onPackageBackedUp(packageInfo, BackupType.FULL, size)
@ -314,7 +120,6 @@ class MetadataManagerTest {
),
manager.getPackageMetadata(packageName)
)
assertEquals(time, manager.getLastBackupTime())
assertFalse(updatedMetadata.d2dBackup)
verify {
@ -323,34 +128,16 @@ class MetadataManagerTest {
}
}
@Test
fun `test onPackageBackedUp() with D2D enabled`() {
expectReadFromCache()
every { clock.time() } returns time
expectModifyMetadata(initialMetadata)
every { settingsManager.d2dBackupsEnabled() } returns true
every { context.packageManager } returns packageManager
manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L)
assertTrue(initialMetadata.d2dBackup)
verify {
cacheInputStream.close()
cacheOutputStream.close()
}
}
@Test
fun `test onPackageBackedUp() with filled cache`() {
val cachedPackageName = getRandomString()
val cacheTime = time - 1
val cachedMetadata = initialMetadata.copy(time = cacheTime)
val cachedMetadata = initialMetadata.copy()
cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(cacheTime)
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime)
val updatedMetadata = cachedMetadata.copy(time = time)
val updatedMetadata = cachedMetadata.copy()
updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
updatedMetadata.packageMetadataMap[packageName] =
PackageMetadata(time, state = APK_AND_DATA)
@ -358,11 +145,10 @@ class MetadataManagerTest {
expectReadFromCache()
every { context.packageManager } returns packageManager
every { clock.time() } returns time
expectModifyMetadata(updatedMetadata)
expectWriteToCache(updatedMetadata)
manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L)
assertEquals(time, manager.getLastBackupTime())
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
assertEquals(
updatedMetadata.packageMetadataMap[packageName],
@ -416,7 +202,7 @@ class MetadataManagerTest {
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NO_DATA)
expectReadFromCache()
expectModifyMetadata(updatedMetadata)
expectWriteToCache(updatedMetadata)
manager.onPackageBackupError(packageInfo, NO_DATA, BackupType.KV)
}
@ -429,34 +215,11 @@ class MetadataManagerTest {
every { context.packageManager } returns packageManager
expectReadFromCache()
expectModifyMetadata(updatedMetadata)
expectWriteToCache(updatedMetadata)
manager.onPackageBackupError(packageInfo, WAS_STOPPED)
}
@Test
fun `test getLastBackupTime() on first run`() {
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
assertEquals(0L, manager.getLastBackupTime())
}
@Test
fun `test getLastBackupTime() and getBackupToken() with cached metadata`() {
initialMetadata.time = Random.nextLong()
expectReadFromCache()
assertEquals(initialMetadata.time, manager.getLastBackupTime())
verify { cacheInputStream.close() }
}
private fun expectModifyMetadata(metadata: BackupMetadata) {
every { metadataWriter.write(metadata, storageOutputStream) } just Runs
expectWriteToCache(metadata)
}
private fun expectReadFromCache() {
val byteArray = ByteArray(DEFAULT_BUFFER_SIZE)
every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream

View file

@ -1,76 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.metadata
import android.content.Context
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KEY_SIZE_BYTES
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import javax.crypto.spec.SecretKeySpec
import kotlin.random.Random
@TestInstance(PER_CLASS)
internal class MetadataReadWriteTest {
private val secretKey = SecretKeySpec(
"This is a legacy backup key 1234".toByteArray(), 0, KEY_SIZE_BYTES, "AES"
)
private val context = mockk<Context>()
private val keyManager = KeyManagerTestImpl(secretKey)
private val cipherFactory = CipherFactoryImpl(keyManager)
private val headerReader = HeaderReaderImpl()
private val cryptoImpl = CryptoImpl(context, keyManager, cipherFactory, headerReader)
private val writer = MetadataWriterImpl(cryptoImpl)
private val reader = MetadataReaderImpl(cryptoImpl)
private val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(Random.nextLong(), APK_AND_DATA, BackupType.FULL))
put(getRandomString(), PackageMetadata(Random.nextLong(), WAS_STOPPED, BackupType.KV))
}
@Test
fun `written metadata matches read metadata`() {
val metadata = getMetadata(packages)
val outputStream = ByteArrayOutputStream()
writer.write(metadata, outputStream)
val inputStream = ByteArrayInputStream(outputStream.toByteArray())
assertEquals(metadata, reader.readMetadata(inputStream, metadata.token))
}
private fun getMetadata(
packageMetadata: HashMap<String, PackageMetadata> = HashMap(),
): BackupMetadata {
return BackupMetadata(
version = VERSION,
token = Random.nextLong(),
salt = getRandomBase64(32),
time = Random.nextLong(),
androidVersion = Random.nextInt(),
androidIncremental = getRandomString(),
deviceName = getRandomString(),
packageMetadataMap = packageMetadata
)
}
}

View file

@ -9,16 +9,10 @@ import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import io.mockk.mockk
import org.json.JSONArray
import org.json.JSONObject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
@ -29,7 +23,7 @@ class MetadataReaderTest {
private val crypto = mockk<Crypto>()
private val encoder = MetadataWriterImpl(crypto)
private val encoder = MetadataWriterImpl()
private val decoder = MetadataReaderImpl(crypto)
private val metadata = getMetadata()
@ -49,11 +43,6 @@ class MetadataReaderTest {
}
}
@Test
fun `expected version and token do not throw SecurityException`() {
decoder.decode(metadataByteArray, metadata.version, metadata.token)
}
@Test
fun `malformed JSON throws SecurityException`() {
assertThrows(SecurityException::class.java) {
@ -61,22 +50,6 @@ class MetadataReaderTest {
}
}
@Test
fun `missing fields throws SecurityException`() {
val json = JSONObject().apply {
put(JSON_METADATA, JSONObject().apply {
put(JSON_METADATA_VERSION, metadata.version.toInt())
put(JSON_METADATA_TOKEN, metadata.token)
put(JSON_METADATA_SDK_INT, metadata.androidVersion)
})
}
val jsonBytes = json.toString().toByteArray(Utf8)
assertThrows(SecurityException::class.java) {
decoder.decode(jsonBytes, metadata.version, metadata.token)
}
}
@Test
fun `missing meta throws SecurityException`() {
val json = JSONObject().apply {
@ -89,26 +62,6 @@ class MetadataReaderTest {
}
}
@Test
fun `package metadata gets read`() {
val packageMetadata = HashMap<String, PackageMetadata>().apply {
put(
"org.example", PackageMetadata(
time = Random.nextLong(),
state = QUOTA_EXCEEDED,
backupType = BackupType.FULL,
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
signatures = listOf(getRandomString(), getRandomString())
)
)
}
val metadata = getMetadata(packageMetadata)
val metadataByteArray = encoder.encode(metadata)
decoder.decode(metadataByteArray, metadata.version, metadata.token)
}
@Test
fun `package metadata with missing time throws`() {
val json = JSONObject(metadataByteArray.toString(Utf8))
@ -124,55 +77,6 @@ class MetadataReaderTest {
}
}
@Test
fun `package metadata unknown state gets mapped to error`() {
val json = JSONObject(metadataByteArray.toString(Utf8))
json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_TIME, Random.nextLong())
put(JSON_PACKAGE_STATE, getRandomString())
put(JSON_PACKAGE_BACKUP_TYPE, BackupType.FULL.name)
put(JSON_PACKAGE_VERSION, Random.nextLong())
put(JSON_PACKAGE_INSTALLER, getRandomString())
put(JSON_PACKAGE_SHA256, getRandomString())
put(JSON_PACKAGE_SIGNATURES, JSONArray(listOf(getRandomString(), getRandomString())))
})
val jsonBytes = json.toString().toByteArray(Utf8)
val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertEquals(this.metadata.salt, metadata.salt)
assertEquals(UNKNOWN_ERROR, metadata.packageMetadataMap["org.example"]!!.state)
assertEquals(BackupType.FULL, metadata.packageMetadataMap["org.example"]!!.backupType)
}
@Test
fun `package metadata missing system gets mapped to false`() {
val json = JSONObject(metadataByteArray.toString(Utf8))
json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_TIME, Random.nextLong())
})
val jsonBytes = json.toString().toByteArray(Utf8)
val metadata = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertFalse(metadata.packageMetadataMap["org.example"]!!.system)
assertNull(metadata.packageMetadataMap["org.example"]!!.backupType)
}
@Test
fun `package metadata can only include time`() {
val json = JSONObject(metadataByteArray.toString(Utf8))
json.put("org.example", JSONObject().apply {
put(JSON_PACKAGE_TIME, Random.nextLong())
put(JSON_PACKAGE_BACKUP_TYPE, BackupType.KV.name)
})
val jsonBytes = json.toString().toByteArray(Utf8)
val result = decoder.decode(jsonBytes, metadata.version, metadata.token)
assertEquals(1, result.packageMetadataMap.size)
val packageMetadata = result.packageMetadataMap.getOrElse("org.example") { fail() }
assertEquals(BackupType.KV, packageMetadata.backupType)
assertNull(packageMetadata.version)
assertNull(packageMetadata.installer)
assertNull(packageMetadata.signatures)
}
private fun getMetadata(
packageMetadata: PackageMetadataMap = PackageMetadataMap(),
): BackupMetadata {

View file

@ -1,161 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 The Calyx Institute
* SPDX-License-Identifier: Apache-2.0
*/
package com.stevesoltys.seedvault.metadata
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import kotlin.random.Random
import kotlin.random.nextLong
@TestInstance(PER_CLASS)
internal class MetadataWriterDecoderTest {
private val crypto = mockk<Crypto>()
private val encoder = MetadataWriterImpl(crypto)
private val decoder = MetadataReaderImpl(crypto)
@Test
fun `encoded metadata matches decoded metadata (no packages)`() {
val metadata = getMetadata().let {
if (it.version == 0.toByte()) it.copy(salt = "") // no salt in version 0
else it
}
assertEquals(
metadata,
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
)
}
@Test
fun `encoded metadata matches decoded metadata (with package, no apk info)`() {
val time = Random.nextLong()
val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(time, APK_AND_DATA, BackupType.FULL))
put(getRandomString(), PackageMetadata(time, WAS_STOPPED, BackupType.KV))
}
val metadata = getMetadata(packages)
assertEquals(
metadata,
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
)
}
@Test
fun `encoded metadata matches decoded metadata (full package)`() {
val packages = HashMap<String, PackageMetadata>().apply {
put(
getRandomString(), PackageMetadata(
time = Random.nextLong(),
state = APK_AND_DATA,
backupType = BackupType.FULL,
size = Random.nextLong(0, Long.MAX_VALUE),
name = getRandomString(),
system = Random.nextBoolean(),
isLaunchableSystemApp = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
splits = listOf(
ApkSplit(getRandomString(), null, getRandomString()),
ApkSplit(getRandomString(), 0L, getRandomString()),
ApkSplit(
name = getRandomString(),
size = Random.nextLong(0, Long.MAX_VALUE),
sha256 = getRandomString(),
),
),
sha256 = getRandomString(),
signatures = listOf(getRandomString(), getRandomString())
)
)
}
val metadata = getMetadata(packages)
assertEquals(
metadata,
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
)
}
@Test
fun `encoded metadata matches decoded metadata (three full packages)`() {
val packages = HashMap<String, PackageMetadata>().apply {
put(
getRandomString(), PackageMetadata(
time = Random.nextLong(),
state = QUOTA_EXCEEDED,
backupType = BackupType.FULL,
name = null,
size = Random.nextLong(0..Long.MAX_VALUE),
system = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
signatures = listOf(getRandomString()),
)
)
put(
getRandomString(), PackageMetadata(
time = Random.nextLong(),
state = NO_DATA,
backupType = BackupType.KV,
size = null,
name = getRandomString(),
system = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
signatures = listOf(getRandomString(), getRandomString()),
)
)
put(
getRandomString(), PackageMetadata(
time = 0L,
state = NOT_ALLOWED,
size = 0,
system = Random.nextBoolean(),
isLaunchableSystemApp = Random.nextBoolean(),
version = Random.nextLong(),
installer = getRandomString(),
sha256 = getRandomString(),
signatures = listOf(getRandomString(), getRandomString()),
)
)
}
val metadata = getMetadata(packages)
assertEquals(
metadata,
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
)
}
private fun getMetadata(
packageMetadata: HashMap<String, PackageMetadata> = HashMap(),
): BackupMetadata {
val version = Random.nextBytes(1)[0]
return BackupMetadata(
version = version,
token = Random.nextLong(),
salt = if (version != 0.toByte()) getRandomBase64(32) else "",
time = Random.nextLong(),
androidVersion = Random.nextInt(),
androidIncremental = getRandomString(),
deviceName = getRandomString(),
packageMetadataMap = packageMetadata
)
}
}

View file

@ -147,7 +147,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val inputStream = CapturingSlot<InputStream>()
val bOutputStream = ByteArrayOutputStream()
every { metadataManager.requiresInit } returns false
every { backupReceiver.assertFinalized() } just Runs
// read one key/value record and write it to output stream
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
@ -217,7 +216,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val appData = ByteArray(size).apply { Random.nextBytes(this) }
val bOutputStream = ByteArrayOutputStream()
every { metadataManager.requiresInit } returns false
every { backupReceiver.assertFinalized() } just Runs
// read one key/value record and write it to output stream
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput

View file

@ -5,14 +5,11 @@
package com.stevesoltys.seedvault.transport.backup
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.content.pm.PackageInfo
import android.net.Uri
import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupType
@ -83,22 +80,6 @@ internal class BackupCoordinatorTest : BackupTest() {
assertEquals(TRANSPORT_OK, backup.finishBackup())
}
@Test
fun `performIncrementalBackup of @pm@ causes re-init when legacy format`() = runBlocking {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
every { backendManager.canDoBackupNow() } returns true
every { metadataManager.requiresInit } returns true
every { data.close() } just Runs
// returns TRANSPORT_NOT_INITIALIZED to re-init next time
assertEquals(
TRANSPORT_NOT_INITIALIZED,
backup.performIncrementalBackup(packageInfo, data, 0)
)
}
@Test
fun `getBackupQuota() delegates to right plugin`() = runBlocking {
val isFullBackup = Random.nextBoolean()
@ -199,7 +180,6 @@ internal class BackupCoordinatorTest : BackupTest() {
coEvery {
full.performFullBackup(packageInfo, fileDescriptor, 0)
} returns TRANSPORT_OK
expectApkBackupAndMetadataWrite()
every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP
every {
full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1)
@ -245,7 +225,6 @@ internal class BackupCoordinatorTest : BackupTest() {
coEvery {
full.performFullBackup(packageInfo, fileDescriptor, 0)
} returns TRANSPORT_OK
expectApkBackupAndMetadataWrite()
every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP
every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED
every { full.currentPackageInfo } returns packageInfo
@ -285,9 +264,4 @@ internal class BackupCoordinatorTest : BackupTest() {
fun `not allowed apps get their APKs backed up after @pm@ backup`() = runBlocking {
}
private fun expectApkBackupAndMetadataWrite() {
coEvery { apkBackup.backupApkIfNecessary(packageInfo, snapshot) } just Runs
every { metadataManager.onApkBackedUp(any(), packageMetadata) } just Runs
}
}

View file

@ -207,7 +207,6 @@ internal class ApkBackupManagerTest : TransportTest() {
} just Runs
// was backed up, get new packageMetadata
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], snapshot) } just Runs
every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata) } just Runs
every { nm.onApkBackupDone() } just Runs