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() {
|
fun resetApplicationState() {
|
||||||
backupManager.setAutoRestore(false)
|
backupManager.setAutoRestore(false)
|
||||||
settingsManager.token = null
|
|
||||||
|
|
||||||
val sharedPreferences = permitDiskReads {
|
val sharedPreferences = permitDiskReads {
|
||||||
PreferenceManager.getDefaultSharedPreferences(targetContext)
|
PreferenceManager.getDefaultSharedPreferences(targetContext)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.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 {
|
||||||
|
|
|
@ -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 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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue