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:
parent
a268116e06
commit
237fd683bd
21 changed files with 82 additions and 793 deletions
|
@ -85,7 +85,6 @@ internal interface LargeTestBase : KoinComponent {
|
|||
|
||||
fun resetApplicationState() {
|
||||
backupManager.setAutoRestore(false)
|
||||
settingsManager.token = null
|
||||
|
||||
val sharedPreferences = permitDiskReads {
|
||||
PreferenceManager.getDefaultSharedPreferences(targetContext)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()) }
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue