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() { fun resetApplicationState() {
backupManager.setAutoRestore(false) backupManager.setAutoRestore(false)
settingsManager.token = null
val sharedPreferences = permitDiskReads { val sharedPreferences = permitDiskReads {
PreferenceManager.getDefaultSharedPreferences(targetContext) PreferenceManager.getDefaultSharedPreferences(targetContext)

View file

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

View file

@ -11,13 +11,8 @@ import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread 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.Clock
import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA 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.PackageService
import com.stevesoltys.seedvault.transport.backup.isSystemApp import com.stevesoltys.seedvault.transport.backup.isSystemApp
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -37,7 +32,6 @@ internal class MetadataManager(
private val metadataWriter: MetadataWriter, private val metadataWriter: MetadataWriter,
private val metadataReader: MetadataReader, private val metadataReader: MetadataReader,
private val packageService: PackageService, private val packageService: PackageService,
private val settingsManager: SettingsManager,
) { ) {
private val uninitializedMetadata = BackupMetadata(token = -42L, salt = "foo bar") private val uninitializedMetadata = BackupMetadata(token = -42L, salt = "foo bar")
@ -54,7 +48,6 @@ internal class MetadataManager(
// This should cause requiresInit() return true // This should cause requiresInit() return true
uninitializedMetadata.copy(version = (-1).toByte()) uninitializedMetadata.copy(version = (-1).toByte())
} }
mLastBackupTime.postValue(field.time)
} }
return field return field
} }
@ -63,40 +56,6 @@ internal class MetadataManager(
packageService.launchableSystemApps.map { it.activityInfo.packageName }.toSet() 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. * Call this after a package has been backed up successfully.
* *
@ -115,8 +74,6 @@ internal class MetadataManager(
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
modifyCachedMetadata { modifyCachedMetadata {
val now = clock.time() val now = clock.time()
metadata.time = now
metadata.d2dBackup = settingsManager.d2dBackupsEnabled()
metadata.packageMetadataMap.getOrPut(packageName) { metadata.packageMetadataMap.getOrPut(packageName) {
val isSystemApp = packageInfo.isSystemApp() val isSystemApp = packageInfo.isSystemApp()
PackageMetadata( PackageMetadata(
@ -124,7 +81,6 @@ internal class MetadataManager(
state = APK_AND_DATA, state = APK_AND_DATA,
backupType = type, backupType = type,
size = size, size = size,
name = packageInfo.applicationInfo?.loadLabel(context.packageManager),
system = isSystemApp, system = isSystemApp,
isLaunchableSystemApp = isSystemApp && isLaunchableSystemApp = isSystemApp &&
launchableSystemApps.contains(packageName), launchableSystemApps.contains(packageName),
@ -135,10 +91,6 @@ internal class MetadataManager(
backupType = type backupType = type
// don't override a previous K/V size, if there were no K/V changes // don't override a previous K/V size, if there were no K/V changes
if (size != null) this.size = size 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) @Throws(IOException::class)
private fun modifyCachedMetadata(modFun: () -> Unit) { 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), packageMetadataMap = PackageMetadataMap(metadata.packageMetadataMap),
) )
try { try {
@ -217,34 +175,6 @@ internal class MetadataManager(
metadata = oldMetadata metadata = oldMetadata
throw IOException(e) 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 @Synchronized

View file

@ -9,7 +9,7 @@ import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module import org.koin.dsl.module
val metadataModule = module { val metadataModule = module {
single { MetadataManager(androidContext(), get(), get(), get(), get(), get()) } single { MetadataManager(androidContext(), get(), get(), get(), get()) }
single<MetadataWriter> { MetadataWriterImpl(get()) } single<MetadataWriter> { MetadataWriterImpl() }
single<MetadataReader> { MetadataReaderImpl(get()) } single<MetadataReader> { MetadataReaderImpl(get()) }
} }

View file

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

View file

@ -6,42 +6,18 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException
import java.io.OutputStream
interface MetadataWriter { interface MetadataWriter {
@Throws(IOException::class)
fun write(metadata: BackupMetadata, outputStream: OutputStream)
fun encode(metadata: BackupMetadata): ByteArray fun encode(metadata: BackupMetadata): ByteArray
} }
internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter { internal class MetadataWriterImpl : 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))
}
}
override fun encode(metadata: BackupMetadata): ByteArray { override fun encode(metadata: BackupMetadata): ByteArray {
val json = JSONObject().apply { val json = JSONObject().apply {
put(JSON_METADATA, JSONObject().apply { put(JSON_METADATA, JSONObject())
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)
})
} }
for ((packageName, packageMetadata) in metadata.packageMetadataMap) { for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
json.put(packageName, JSONObject().apply { json.put(packageName, JSONObject().apply {
@ -57,31 +33,14 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
if (packageMetadata.size != null) { if (packageMetadata.size != null) {
put(JSON_PACKAGE_SIZE, packageMetadata.size) put(JSON_PACKAGE_SIZE, packageMetadata.size)
} }
if (packageMetadata.name != null) {
put(JSON_PACKAGE_APP_NAME, packageMetadata.name)
}
if (packageMetadata.system) { if (packageMetadata.system) {
put(JSON_PACKAGE_SYSTEM, true) put(JSON_PACKAGE_SYSTEM, true)
} }
if (packageMetadata.isLaunchableSystemApp) { if (packageMetadata.isLaunchableSystemApp) {
put(JSON_PACKAGE_SYSTEM_LAUNCHER, true) 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) return json.toString().toByteArray(Utf8)
} }
} }

View file

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

View file

@ -88,7 +88,7 @@ internal class SettingsViewModel(
private val mBackupPossible = MutableLiveData(false) private val mBackupPossible = MutableLiveData(false)
val backupPossible: LiveData<Boolean> = mBackupPossible val backupPossible: LiveData<Boolean> = mBackupPossible
internal val lastBackupTime = metadataManager.lastBackupTime internal val lastBackupTime = settingsManager.lastBackupTime
internal val appBackupWorkInfo = internal val appBackupWorkInfo =
workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map { workManager.getWorkInfosForUniqueWorkLiveData(UNIQUE_WORK_NAME).map {
it.getOrNull(0) it.getOrNull(0)
@ -143,8 +143,6 @@ internal class SettingsViewModel(
initialValue = false, initialValue = false,
) )
scope.launch { scope.launch {
// ensures the lastBackupTime LiveData gets set
metadataManager.getLastBackupTime()
// update running state // update running state
isBackupRunning.collect { isBackupRunning.collect {
onBackupRunningStateChanged() onBackupRunningStateChanged()
@ -258,21 +256,6 @@ internal class SettingsViewModel(
fun onBackupEnabled(enabled: Boolean) { fun onBackupEnabled(enabled: Boolean) {
if (enabled) { 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) // enable call log backups for existing installs (added end of 2020)
enableCallLogBackup() enableCallLogBackup()
} else { } else {

View file

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

View file

@ -199,15 +199,6 @@ internal class BackupCoordinator(
flags: Int, flags: Int,
): Int { ): Int {
state.cancelReason = UNKNOWN_ERROR 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) return kv.performBackup(packageInfo, data, flags)
} }
@ -324,8 +315,6 @@ internal class BackupCoordinator(
// tell K/V backup to finish // tell K/V backup to finish
val backupData = kv.finishBackup() val backupData = kv.finishBackup()
snapshotCreator.onPackageBackedUp(packageInfo, BackupType.KV, backupData) snapshotCreator.onPackageBackedUp(packageInfo, BackupType.KV, backupData)
// TODO unify both calls
metadataManager.onPackageBackedUp(packageInfo, BackupType.KV, backupData.size)
TRANSPORT_OK TRANSPORT_OK
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error finishing K/V backup for $packageName", e) Log.e(TAG, "Error finishing K/V backup for $packageName", e)
@ -345,8 +334,6 @@ internal class BackupCoordinator(
try { try {
val backupData = full.finishBackup() val backupData = full.finishBackup()
snapshotCreator.onPackageBackedUp(packageInfo, BackupType.FULL, backupData) snapshotCreator.onPackageBackedUp(packageInfo, BackupType.FULL, backupData)
// TODO unify both calls
metadataManager.onPackageBackedUp(packageInfo, BackupType.FULL, backupData.size)
TRANSPORT_OK TRANSPORT_OK
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e) Log.e(TAG, "Error calling onPackageBackedUp for $packageName", e)
@ -362,7 +349,6 @@ internal class BackupCoordinator(
else -> throw IllegalStateException("Unexpected state in finishBackup()") 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) { private fun onPackageBackupError(packageInfo: PackageInfo, type: BackupType) {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
try { try {

View file

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

View file

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

View file

@ -11,7 +11,7 @@ import android.text.format.DateUtils.getRelativeTimeSpanString
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
fun Long.toRelativeTime(context: Context): CharSequence { 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) context.getString(R.string.settings_backup_last_backup_never)
} else { } else {
val now = System.currentTimeMillis() 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.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.AppBackupManager import com.stevesoltys.seedvault.transport.backup.AppBackupManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.backup.hexFromProto
import com.stevesoltys.seedvault.worker.BackupRequester import com.stevesoltys.seedvault.worker.BackupRequester
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -138,18 +139,25 @@ internal class NotificationBackupObserver(
Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status") Log.i(TAG, "Backup finished $numPackages/$requestedPackages. Status: $status")
} }
var success = status == 0 var success = status == 0
val size = if (success) metadataManager.getPackagesBackupSize() else 0L
val total = try { val total = try {
packageService.allUserPackages.size packageService.allUserPackages.size
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error getting number of all user packages: ", e) Log.e(TAG, "Error getting number of all user packages: ", e)
requestedPackages requestedPackages
} }
runBlocking { val snapshot = runBlocking {
check(!Looper.getMainLooper().isCurrentThread) check(!Looper.getMainLooper().isCurrentThread)
Log.d(TAG, "Finalizing backup...") 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) 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
import android.content.Context.MODE_PRIVATE import android.content.Context.MODE_PRIVATE
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.TestApp import com.stevesoltys.seedvault.TestApp
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA 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.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
@ -37,22 +32,16 @@ import io.mockk.verify
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.jupiter.api.assertThrows
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.core.context.stopKoin import org.koin.core.context.stopKoin
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException
import kotlin.random.Random import kotlin.random.Random
@Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@Config( @Config(
sdk = [34], // TODO: Drop once robolectric supports 35 sdk = [34], // TODO: Drop once robolectric supports 35
@ -62,7 +51,6 @@ class MetadataManagerTest {
private val context: Context = mockk() private val context: Context = mockk()
private val clock: Clock = mockk() private val clock: Clock = mockk()
private val crypto: Crypto = mockk()
private val metadataWriter: MetadataWriter = mockk() private val metadataWriter: MetadataWriter = mockk()
private val metadataReader: MetadataReader = mockk() private val metadataReader: MetadataReader = mockk()
private val packageService: PackageService = mockk() private val packageService: PackageService = mockk()
@ -74,7 +62,6 @@ class MetadataManagerTest {
metadataWriter = metadataWriter, metadataWriter = metadataWriter,
metadataReader = metadataReader, metadataReader = metadataReader,
packageService = packageService, packageService = packageService,
settingsManager = settingsManager,
) )
private val packageManager: PackageManager = mockk() private val packageManager: PackageManager = mockk()
@ -104,187 +91,6 @@ class MetadataManagerTest {
stopKoin() 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 @Test
fun `test onPackageBackedUp()`() { fun `test onPackageBackedUp()`() {
packageInfo.applicationInfo!!.flags = FLAG_SYSTEM packageInfo.applicationInfo!!.flags = FLAG_SYSTEM
@ -300,7 +106,7 @@ class MetadataManagerTest {
every { packageService.launchableSystemApps } returns emptyList() every { packageService.launchableSystemApps } returns emptyList()
expectReadFromCache() expectReadFromCache()
every { clock.time() } returns time every { clock.time() } returns time
expectModifyMetadata(initialMetadata) expectWriteToCache(initialMetadata)
manager.onPackageBackedUp(packageInfo, BackupType.FULL, size) manager.onPackageBackedUp(packageInfo, BackupType.FULL, size)
@ -314,7 +120,6 @@ class MetadataManagerTest {
), ),
manager.getPackageMetadata(packageName) manager.getPackageMetadata(packageName)
) )
assertEquals(time, manager.getLastBackupTime())
assertFalse(updatedMetadata.d2dBackup) assertFalse(updatedMetadata.d2dBackup)
verify { 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 @Test
fun `test onPackageBackedUp() with filled cache`() { fun `test onPackageBackedUp() with filled cache`() {
val cachedPackageName = getRandomString() val cachedPackageName = getRandomString()
val cacheTime = time - 1 val cacheTime = time - 1
val cachedMetadata = initialMetadata.copy(time = cacheTime) val cachedMetadata = initialMetadata.copy()
cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(cacheTime) cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(cacheTime)
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime) cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime)
val updatedMetadata = cachedMetadata.copy(time = time) val updatedMetadata = cachedMetadata.copy()
updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time) updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
updatedMetadata.packageMetadataMap[packageName] = updatedMetadata.packageMetadataMap[packageName] =
PackageMetadata(time, state = APK_AND_DATA) PackageMetadata(time, state = APK_AND_DATA)
@ -358,11 +145,10 @@ class MetadataManagerTest {
expectReadFromCache() expectReadFromCache()
every { context.packageManager } returns packageManager every { context.packageManager } returns packageManager
every { clock.time() } returns time every { clock.time() } returns time
expectModifyMetadata(updatedMetadata) expectWriteToCache(updatedMetadata)
manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L) manager.onPackageBackedUp(packageInfo, BackupType.FULL, 0L)
assertEquals(time, manager.getLastBackupTime())
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName)) assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
assertEquals( assertEquals(
updatedMetadata.packageMetadataMap[packageName], updatedMetadata.packageMetadataMap[packageName],
@ -416,7 +202,7 @@ class MetadataManagerTest {
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NO_DATA) updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(state = NO_DATA)
expectReadFromCache() expectReadFromCache()
expectModifyMetadata(updatedMetadata) expectWriteToCache(updatedMetadata)
manager.onPackageBackupError(packageInfo, NO_DATA, BackupType.KV) manager.onPackageBackupError(packageInfo, NO_DATA, BackupType.KV)
} }
@ -429,34 +215,11 @@ class MetadataManagerTest {
every { context.packageManager } returns packageManager every { context.packageManager } returns packageManager
expectReadFromCache() expectReadFromCache()
expectModifyMetadata(updatedMetadata) expectWriteToCache(updatedMetadata)
manager.onPackageBackupError(packageInfo, WAS_STOPPED) 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() { private fun expectReadFromCache() {
val byteArray = ByteArray(DEFAULT_BUFFER_SIZE) val byteArray = ByteArray(DEFAULT_BUFFER_SIZE)
every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream 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.crypto.Crypto
import com.stevesoltys.seedvault.getRandomBase64 import com.stevesoltys.seedvault.getRandomBase64
import com.stevesoltys.seedvault.getRandomString 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 io.mockk.mockk
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject 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.assertThrows
import org.junit.jupiter.api.Assertions.fail
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
@ -29,7 +23,7 @@ class MetadataReaderTest {
private val crypto = mockk<Crypto>() private val crypto = mockk<Crypto>()
private val encoder = MetadataWriterImpl(crypto) private val encoder = MetadataWriterImpl()
private val decoder = MetadataReaderImpl(crypto) private val decoder = MetadataReaderImpl(crypto)
private val metadata = getMetadata() 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 @Test
fun `malformed JSON throws SecurityException`() { fun `malformed JSON throws SecurityException`() {
assertThrows(SecurityException::class.java) { 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 @Test
fun `missing meta throws SecurityException`() { fun `missing meta throws SecurityException`() {
val json = JSONObject().apply { 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 @Test
fun `package metadata with missing time throws`() { fun `package metadata with missing time throws`() {
val json = JSONObject(metadataByteArray.toString(Utf8)) 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( private fun getMetadata(
packageMetadata: PackageMetadataMap = PackageMetadataMap(), packageMetadata: PackageMetadataMap = PackageMetadataMap(),
): BackupMetadata { ): 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 inputStream = CapturingSlot<InputStream>()
val bOutputStream = ByteArrayOutputStream() val bOutputStream = ByteArrayOutputStream()
every { metadataManager.requiresInit } returns false
every { backupReceiver.assertFinalized() } just Runs every { backupReceiver.assertFinalized() } just Runs
// read one key/value record and write it to output stream // read one key/value record and write it to output stream
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
@ -217,7 +216,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val appData = ByteArray(size).apply { Random.nextBytes(this) } val appData = ByteArray(size).apply { Random.nextBytes(this) }
val bOutputStream = ByteArrayOutputStream() val bOutputStream = ByteArrayOutputStream()
every { metadataManager.requiresInit } returns false
every { backupReceiver.assertFinalized() } just Runs every { backupReceiver.assertFinalized() } just Runs
// read one key/value record and write it to output stream // read one key/value record and write it to output stream
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput

View file

@ -5,14 +5,11 @@
package com.stevesoltys.seedvault.transport.backup 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_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.backend.BackendManager import com.stevesoltys.seedvault.backend.BackendManager
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupType import com.stevesoltys.seedvault.metadata.BackupType
@ -83,22 +80,6 @@ internal class BackupCoordinatorTest : BackupTest() {
assertEquals(TRANSPORT_OK, backup.finishBackup()) 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 @Test
fun `getBackupQuota() delegates to right plugin`() = runBlocking { fun `getBackupQuota() delegates to right plugin`() = runBlocking {
val isFullBackup = Random.nextBoolean() val isFullBackup = Random.nextBoolean()
@ -199,7 +180,6 @@ internal class BackupCoordinatorTest : BackupTest() {
coEvery { coEvery {
full.performFullBackup(packageInfo, fileDescriptor, 0) full.performFullBackup(packageInfo, fileDescriptor, 0)
} returns TRANSPORT_OK } returns TRANSPORT_OK
expectApkBackupAndMetadataWrite()
every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP
every { every {
full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1)
@ -245,7 +225,6 @@ internal class BackupCoordinatorTest : BackupTest() {
coEvery { coEvery {
full.performFullBackup(packageInfo, fileDescriptor, 0) full.performFullBackup(packageInfo, fileDescriptor, 0)
} returns TRANSPORT_OK } returns TRANSPORT_OK
expectApkBackupAndMetadataWrite()
every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP every { full.quota } returns DEFAULT_QUOTA_FULL_BACKUP
every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED
every { full.currentPackageInfo } returns packageInfo 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 { 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 } just Runs
// was backed up, get new packageMetadata // was backed up, get new packageMetadata
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], snapshot) } just Runs coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], snapshot) } just Runs
every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata) } just Runs
every { nm.onApkBackupDone() } just Runs every { nm.onApkBackupDone() } just Runs