Also back up APKs of apps that have no data or are above quota
This should also affect apps that have other errors during the backup process, but it does not affect apps that opt-out of backup completely. First part of #65
This commit is contained in:
parent
3f73119b52
commit
3d296e1335
14 changed files with 312 additions and 90 deletions
|
@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.metadata
|
|||
|
||||
import android.os.Build
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import java.io.InputStream
|
||||
|
||||
typealias PackageMetadataMap = HashMap<String, PackageMetadata>
|
||||
|
@ -24,8 +25,33 @@ internal const val JSON_METADATA_SDK_INT = "sdk_int"
|
|||
internal const val JSON_METADATA_INCREMENTAL = "incremental"
|
||||
internal const val JSON_METADATA_NAME = "name"
|
||||
|
||||
enum class PackageState {
|
||||
/**
|
||||
* Both, the APK and the package's data was backed up.
|
||||
* This is the expected state of all user-installed packages.
|
||||
*/
|
||||
APK_AND_DATA,
|
||||
/**
|
||||
* Package data could not get backed up, because the app exceeded the allowed quota.
|
||||
*/
|
||||
QUOTA_EXCEEDED,
|
||||
/**
|
||||
* Package data could not get backed up, because the app reported no data to back up.
|
||||
*/
|
||||
NO_DATA,
|
||||
/**
|
||||
* Package data could not get backed up, because an error occurred during backup.
|
||||
*/
|
||||
UNKNOWN_ERROR,
|
||||
}
|
||||
|
||||
data class PackageMetadata(
|
||||
internal var time: Long,
|
||||
/**
|
||||
* The timestamp in milliseconds of the last app data backup.
|
||||
* It is 0 if there never was a data backup.
|
||||
*/
|
||||
internal var time: Long = 0L,
|
||||
internal var state: PackageState = UNKNOWN_ERROR,
|
||||
internal val version: Long? = null,
|
||||
internal val installer: String? = null,
|
||||
internal val sha256: String? = null,
|
||||
|
@ -37,6 +63,7 @@ data class PackageMetadata(
|
|||
}
|
||||
|
||||
internal const val JSON_PACKAGE_TIME = "time"
|
||||
internal const val JSON_PACKAGE_STATE = "state"
|
||||
internal const val JSON_PACKAGE_VERSION = "version"
|
||||
internal const val JSON_PACKAGE_INSTALLER = "installer"
|
||||
internal const val JSON_PACKAGE_SHA256 = "sha256"
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.util.Log
|
|||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
@ -44,22 +45,21 @@ class MetadataManager(
|
|||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
|
||||
metadata = BackupMetadata(token = token)
|
||||
metadataWriter.write(metadata, metadataOutputStream)
|
||||
writeMetadataToCache()
|
||||
modifyMetadata(metadataOutputStream) {
|
||||
metadata = BackupMetadata(token = token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this after an APK as been successfully written to backup storage.
|
||||
* It will update the package's metadata, but NOT write it storage or internal cache.
|
||||
* You still need to call [onPackageBackedUp] afterwards to write it out.
|
||||
* Call this after a package's APK has been backed up successfully.
|
||||
*
|
||||
* It updates the packages' metadata
|
||||
* and writes it encrypted to the given [OutputStream] as well as the internal cache.
|
||||
*/
|
||||
@Synchronized
|
||||
fun onApkBackedUp(packageName: String, packageMetadata: PackageMetadata) {
|
||||
@Throws(IOException::class)
|
||||
fun onApkBackedUp(packageName: String, packageMetadata: PackageMetadata, metadataOutputStream: OutputStream) {
|
||||
metadata.packageMetadataMap[packageName]?.let {
|
||||
check(it.time <= packageMetadata.time) {
|
||||
"APK backup set time of $packageName backwards"
|
||||
}
|
||||
check(packageMetadata.version != null) {
|
||||
"APK backup returned version null"
|
||||
}
|
||||
|
@ -67,7 +67,16 @@ class MetadataManager(
|
|||
"APK backup backed up the same or a smaller version: was ${it.version} is ${packageMetadata.version}"
|
||||
}
|
||||
}
|
||||
metadata.packageMetadataMap[packageName] = packageMetadata
|
||||
modifyMetadata(metadataOutputStream) {
|
||||
val oldPackageMetadata = metadata.packageMetadataMap[packageName]
|
||||
?: PackageMetadata()
|
||||
metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy(
|
||||
version = packageMetadata.version,
|
||||
installer = packageMetadata.installer,
|
||||
sha256 = packageMetadata.sha256,
|
||||
signatures = packageMetadata.signatures
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,24 +88,56 @@ class MetadataManager(
|
|||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
fun onPackageBackedUp(packageName: String, metadataOutputStream: OutputStream) {
|
||||
val oldMetadata = metadata.copy()
|
||||
val now = clock.time()
|
||||
metadata.time = now
|
||||
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
||||
metadata.packageMetadataMap[packageName]?.time = now
|
||||
} else {
|
||||
metadata.packageMetadataMap[packageName] = PackageMetadata(time = now)
|
||||
modifyMetadata(metadataOutputStream) {
|
||||
val now = clock.time()
|
||||
metadata.time = now
|
||||
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
||||
metadata.packageMetadataMap[packageName]!!.time = now
|
||||
metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA
|
||||
} else {
|
||||
metadata.packageMetadataMap[packageName] = PackageMetadata(
|
||||
time = now,
|
||||
state = APK_AND_DATA
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this after a package data backup failed.
|
||||
*
|
||||
* It updates the packages' metadata
|
||||
* and writes it encrypted to the given [OutputStream] as well as the internal cache.
|
||||
*/
|
||||
@Synchronized
|
||||
@Throws(IOException::class)
|
||||
internal fun onPackageBackupError(packageName: String, packageState: PackageState, metadataOutputStream: OutputStream) {
|
||||
check(packageState != APK_AND_DATA) { "Backup Error with non-error package state." }
|
||||
modifyMetadata(metadataOutputStream) {
|
||||
if (metadata.packageMetadataMap.containsKey(packageName)) {
|
||||
metadata.packageMetadataMap[packageName]!!.state = packageState
|
||||
} else {
|
||||
metadata.packageMetadataMap[packageName] = PackageMetadata(
|
||||
time = 0L,
|
||||
state = packageState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun modifyMetadata(metadataOutputStream: OutputStream, modFun: () -> Unit) {
|
||||
val oldMetadata = metadata.copy()
|
||||
try {
|
||||
modFun.invoke()
|
||||
metadataWriter.write(metadata, metadataOutputStream)
|
||||
writeMetadataToCache()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Error writing metadata to storage", e)
|
||||
// revert metadata and do not write it to cache
|
||||
// TODO also revert changes made by last [onApkBackedUp]
|
||||
metadata = oldMetadata
|
||||
throw IOException(e)
|
||||
}
|
||||
writeMetadataToCache()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.stevesoltys.seedvault.Utf8
|
|||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.header.UnsupportedVersionException
|
||||
import com.stevesoltys.seedvault.header.VERSION
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
|
@ -59,8 +60,14 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
|||
for (packageName in json.keys()) {
|
||||
if (packageName == JSON_METADATA) continue
|
||||
val p = json.getJSONObject(packageName)
|
||||
val pState = when(p.optString(JSON_PACKAGE_STATE)) {
|
||||
"" -> APK_AND_DATA
|
||||
QUOTA_EXCEEDED.name -> QUOTA_EXCEEDED
|
||||
NO_DATA.name -> NO_DATA
|
||||
else -> UNKNOWN_ERROR
|
||||
}
|
||||
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
|
||||
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER, "")
|
||||
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER)
|
||||
val pSha256 = p.optString(JSON_PACKAGE_SHA256)
|
||||
val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES)
|
||||
val signatures = if (pSignatures == null) null else
|
||||
|
@ -71,6 +78,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
|||
}
|
||||
packageMetadataMap[packageName] = PackageMetadata(
|
||||
time = p.getLong(JSON_PACKAGE_TIME),
|
||||
state = pState,
|
||||
version = if (pVersion == 0L) null else pVersion,
|
||||
installer = if (pInstaller == "") null else pInstaller,
|
||||
sha256 = if (pSha256 == "") null else pSha256,
|
||||
|
|
|
@ -2,6 +2,7 @@ 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
|
||||
|
@ -36,6 +37,9 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
|
|||
for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
|
||||
json.put(packageName, JSONObject().apply {
|
||||
put(JSON_PACKAGE_TIME, packageMetadata.time)
|
||||
if (packageMetadata.state != APK_AND_DATA) {
|
||||
put(JSON_PACKAGE_STATE, packageMetadata.state.name)
|
||||
}
|
||||
packageMetadata.version?.let { put(JSON_PACKAGE_VERSION, it) }
|
||||
packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) }
|
||||
packageMetadata.sha256?.let { put(JSON_PACKAGE_SHA256, it) }
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.content.pm.Signature
|
|||
import android.content.pm.SigningInfo
|
||||
import android.util.Log
|
||||
import android.util.PackageUtils.computeSha256DigestBytes
|
||||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.encodeBase64
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
|
@ -24,43 +23,50 @@ private val TAG = ApkBackup::class.java.simpleName
|
|||
|
||||
class ApkBackup(
|
||||
private val pm: PackageManager,
|
||||
private val clock: Clock,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val metadataManager: MetadataManager) {
|
||||
|
||||
/**
|
||||
* Checks if a new APK needs to get backed up,
|
||||
* because the version code or the signatures have changed.
|
||||
* Only if an APK needs a backup, an [OutputStream] is obtained from the given streamGetter
|
||||
* and the APK binary written to it.
|
||||
*
|
||||
* @return new [PackageMetadata] if an APK backup was made or null if no backup was made.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): Boolean {
|
||||
fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): PackageMetadata? {
|
||||
// do not back up @pm@
|
||||
val packageName = packageInfo.packageName
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER) return false
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER) return null
|
||||
|
||||
// do not back up when setting is not enabled
|
||||
if (!settingsManager.backupApks()) return false
|
||||
if (!settingsManager.backupApks()) return null
|
||||
|
||||
// do not back up system apps that haven't been updated
|
||||
val isSystemApp = packageInfo.applicationInfo.flags and FLAG_SYSTEM != 0
|
||||
val isUpdatedSystemApp = packageInfo.applicationInfo.flags and FLAG_UPDATED_SYSTEM_APP != 0
|
||||
if (isSystemApp && !isUpdatedSystemApp) {
|
||||
Log.d(TAG, "Package $packageName is vanilla system app. Not backing it up.")
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO remove when adding support for packages with multiple signers
|
||||
if (packageInfo.signingInfo.hasMultipleSigners()) {
|
||||
Log.e(TAG, "Package $packageName has multiple signers. Not backing it up.")
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// get signatures
|
||||
val signatures = packageInfo.signingInfo.getSignatures()
|
||||
if (signatures.isEmpty()) {
|
||||
Log.e(TAG, "Package $packageName has no signatures. Not backing it up.")
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// get cached metadata about package
|
||||
val packageMetadata = metadataManager.getPackageMetadata(packageName)
|
||||
?: PackageMetadata(time = clock.time())
|
||||
?: PackageMetadata()
|
||||
|
||||
// get version codes
|
||||
val version = packageInfo.longVersionCode
|
||||
|
@ -69,7 +75,7 @@ class ApkBackup(
|
|||
// do not backup if we have the version already and signatures did not change
|
||||
if (version <= backedUpVersion && !signaturesChanged(packageMetadata, signatures)) {
|
||||
Log.d(TAG, "Package $packageName with version $version already has a backup ($backedUpVersion) with the same signature. Not backing it up.")
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// get an InputStream for the APK
|
||||
|
@ -100,17 +106,13 @@ class ApkBackup(
|
|||
val sha256 = messageDigest.digest().encodeBase64()
|
||||
Log.d(TAG, "Backed up new APK of $packageName with version $version.")
|
||||
|
||||
// update the metadata
|
||||
val installer = pm.getInstallerPackageName(packageName)
|
||||
val updatedMetadata = PackageMetadata(
|
||||
time = clock.time(),
|
||||
// return updated metadata
|
||||
return PackageMetadata(
|
||||
version = version,
|
||||
installer = installer,
|
||||
installer = pm.getInstallerPackageName(packageName),
|
||||
sha256 = sha256,
|
||||
signatures = signatures
|
||||
)
|
||||
metadataManager.onApkBackedUp(packageName, updatedMetadata)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun signaturesChanged(packageMetadata: PackageMetadata, signatures: List<String>): Boolean {
|
||||
|
|
|
@ -9,6 +9,8 @@ import com.stevesoltys.seedvault.BackupNotificationManager
|
|||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||
import com.stevesoltys.seedvault.metadata.PackageState
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
|
@ -32,6 +34,7 @@ internal class BackupCoordinator(
|
|||
|
||||
private var calledInitialize = false
|
||||
private var calledClearBackupData = false
|
||||
private var cancelReason: PackageState = UNKNOWN_ERROR
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
// Transport initialization and quota
|
||||
|
@ -110,13 +113,15 @@ internal class BackupCoordinator(
|
|||
}
|
||||
|
||||
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
||||
cancelReason = UNKNOWN_ERROR
|
||||
val packageName = packageInfo.packageName
|
||||
// backups of package manager metadata do not respect backoff
|
||||
// we need to reject them manually when now is not a good time for a backup
|
||||
if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
|
||||
return TRANSPORT_PACKAGE_REJECTED
|
||||
}
|
||||
return kv.performBackup(packageInfo, data, flags)
|
||||
val result = kv.performBackup(packageInfo, data, flags)
|
||||
return backUpApk(result, packageInfo)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------
|
||||
|
@ -140,10 +145,17 @@ internal class BackupCoordinator(
|
|||
Log.i(TAG, "Request full backup time. Returned $this")
|
||||
}
|
||||
|
||||
fun checkFullBackupSize(size: Long) = full.checkFullBackupSize(size)
|
||||
fun checkFullBackupSize(size: Long): Int {
|
||||
val result = full.checkFullBackupSize(size)
|
||||
if (result == TRANSPORT_PACKAGE_REJECTED) cancelReason = NO_DATA
|
||||
else if (result == TRANSPORT_QUOTA_EXCEEDED) cancelReason = QUOTA_EXCEEDED
|
||||
return result
|
||||
}
|
||||
|
||||
fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
|
||||
return full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
cancelReason = UNKNOWN_ERROR
|
||||
val result = full.performFullBackup(targetPackage, fileDescriptor, flags)
|
||||
return backUpApk(result, targetPackage)
|
||||
}
|
||||
|
||||
fun sendBackupData(numBytes: Int) = full.sendBackupData(numBytes)
|
||||
|
@ -161,7 +173,13 @@ internal class BackupCoordinator(
|
|||
* If the transport receives this callback, it will *not* receive a call to [finishBackup].
|
||||
* It needs to tear down any ongoing backup state here.
|
||||
*/
|
||||
fun cancelFullBackup() = full.cancelFullBackup()
|
||||
fun cancelFullBackup() {
|
||||
val packageInfo = full.getCurrentPackage()
|
||||
?: throw AssertionError("Cancelling full backup, but no current package")
|
||||
Log.i(TAG, "Cancel full backup of ${packageInfo.packageName} because of $cancelReason")
|
||||
onPackageBackupError(packageInfo)
|
||||
full.cancelFullBackup()
|
||||
}
|
||||
|
||||
// Clear and Finish
|
||||
|
||||
|
@ -193,6 +211,14 @@ internal class BackupCoordinator(
|
|||
return TRANSPORT_OK
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish sending application data to the backup destination.
|
||||
* This must be called after [performIncrementalBackup], [performFullBackup], or [clearBackupData]
|
||||
* to ensure that all data is sent and the operation properly finalized.
|
||||
* Only when this method returns true can a backup be assumed to have succeeded.
|
||||
*
|
||||
* @return the same error codes as [performIncrementalBackup] or [performFullBackup].
|
||||
*/
|
||||
fun finishBackup(): Int = when {
|
||||
kv.hasState() -> {
|
||||
check(!full.hasState()) { "K/V backup has state, but full backup has dangling state as well" }
|
||||
|
@ -212,10 +238,25 @@ internal class BackupCoordinator(
|
|||
else -> throw IllegalStateException("Unexpected state in finishBackup()")
|
||||
}
|
||||
|
||||
private fun backUpApk(result: Int, packageInfo: PackageInfo): Int {
|
||||
val packageName = packageInfo.packageName
|
||||
return try {
|
||||
apkBackup.backupApkIfNecessary(packageInfo) {
|
||||
plugin.getApkOutputStream(packageInfo)
|
||||
}?.let { packageMetadata ->
|
||||
val outputStream = plugin.getMetadataOutputStream()
|
||||
metadataManager.onApkBackedUp(packageName, packageMetadata, outputStream)
|
||||
}
|
||||
result
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
|
||||
TRANSPORT_PACKAGE_REJECTED
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPackageBackedUp(packageInfo: PackageInfo) {
|
||||
val packageName = packageInfo.packageName
|
||||
try {
|
||||
apkBackup.backupApkIfNecessary(packageInfo) { plugin.getApkOutputStream(packageInfo) }
|
||||
val outputStream = plugin.getMetadataOutputStream()
|
||||
metadataManager.onPackageBackedUp(packageName, outputStream)
|
||||
} catch (e: IOException) {
|
||||
|
@ -223,6 +264,16 @@ internal class BackupCoordinator(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onPackageBackupError(packageInfo: PackageInfo) {
|
||||
val packageName = packageInfo.packageName
|
||||
try {
|
||||
val outputStream = plugin.getMetadataOutputStream()
|
||||
metadataManager.onPackageBackupError(packageName, cancelReason, outputStream)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Error while writing metadata for $packageName", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBackupBackoff(): Long {
|
||||
val noBackoff = 0L
|
||||
val defaultBackoff = DAYS.toMillis(30)
|
||||
|
|
|
@ -5,7 +5,7 @@ import org.koin.dsl.module
|
|||
|
||||
val backupModule = module {
|
||||
single { InputFactory() }
|
||||
single { ApkBackup(androidContext().packageManager, get(), get(), get()) }
|
||||
single { ApkBackup(androidContext().packageManager, get(), get()) }
|
||||
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
|
||||
single { FullBackup(get<BackupPlugin>().fullBackupPlugin, get(), get(), get()) }
|
||||
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
|
|
|
@ -45,7 +45,7 @@ internal class FullBackup(
|
|||
Log.i(TAG, "Check full backup size of $size bytes.")
|
||||
return when {
|
||||
size <= 0 -> TRANSPORT_PACKAGE_REJECTED
|
||||
size > plugin.getQuota() -> TRANSPORT_QUOTA_EXCEEDED
|
||||
size > getQuota() -> TRANSPORT_QUOTA_EXCEEDED
|
||||
else -> TRANSPORT_OK
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||
import com.stevesoltys.seedvault.Clock
|
||||
import com.stevesoltys.seedvault.getRandomByteArray
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
|
@ -46,8 +47,8 @@ class MetadataManagerTest {
|
|||
@Test
|
||||
fun `test onDeviceInitialization()`() {
|
||||
every { clock.time() } returns time
|
||||
every { metadataWriter.write(initialMetadata, storageOutputStream) } just Runs
|
||||
expectWriteToCache(initialMetadata)
|
||||
expectReadFromCache()
|
||||
expectModifyMetadata(initialMetadata)
|
||||
|
||||
manager.onDeviceInitialization(token, storageOutputStream)
|
||||
|
||||
|
@ -58,15 +59,16 @@ class MetadataManagerTest {
|
|||
@Test
|
||||
fun `test onApkBackedUp() with no prior package metadata`() {
|
||||
val packageMetadata = PackageMetadata(
|
||||
time = time + 1,
|
||||
time = 0L,
|
||||
version = Random.nextLong(Long.MAX_VALUE),
|
||||
installer = getRandomString(),
|
||||
signatures = listOf("sig")
|
||||
)
|
||||
|
||||
expectReadFromCache()
|
||||
expectModifyMetadata(initialMetadata)
|
||||
|
||||
manager.onApkBackedUp(packageName, packageMetadata)
|
||||
manager.onApkBackedUp(packageName, packageMetadata, storageOutputStream)
|
||||
|
||||
assertEquals(packageMetadata, manager.getPackageMetadata(packageName))
|
||||
}
|
||||
|
@ -81,15 +83,16 @@ class MetadataManagerTest {
|
|||
)
|
||||
initialMetadata.packageMetadataMap[packageName] = packageMetadata
|
||||
val updatedPackageMetadata = PackageMetadata(
|
||||
time = time + 1,
|
||||
time = time,
|
||||
version = packageMetadata.version!! + 1,
|
||||
installer = getRandomString(),
|
||||
signatures = listOf("sig foo")
|
||||
)
|
||||
|
||||
expectReadFromCache()
|
||||
expectModifyMetadata(initialMetadata)
|
||||
|
||||
manager.onApkBackedUp(packageName, updatedPackageMetadata)
|
||||
manager.onApkBackedUp(packageName, updatedPackageMetadata, storageOutputStream)
|
||||
|
||||
assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName))
|
||||
}
|
||||
|
@ -102,8 +105,7 @@ class MetadataManagerTest {
|
|||
|
||||
expectReadFromCache()
|
||||
every { clock.time() } returns time
|
||||
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs
|
||||
expectWriteToCache(updatedMetadata)
|
||||
expectModifyMetadata(updatedMetadata)
|
||||
|
||||
manager.onPackageBackedUp(packageName, storageOutputStream)
|
||||
|
||||
|
@ -142,19 +144,18 @@ class MetadataManagerTest {
|
|||
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime)
|
||||
|
||||
val updatedMetadata = cachedMetadata.copy(time = time)
|
||||
cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
|
||||
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(time)
|
||||
updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
|
||||
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(time, state = APK_AND_DATA)
|
||||
|
||||
expectReadFromCache()
|
||||
every { clock.time() } returns time
|
||||
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs
|
||||
expectWriteToCache(updatedMetadata)
|
||||
expectModifyMetadata(updatedMetadata)
|
||||
|
||||
manager.onPackageBackedUp(packageName, storageOutputStream)
|
||||
|
||||
assertEquals(time, manager.getLastBackupTime())
|
||||
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
|
||||
assertEquals(PackageMetadata(time), manager.getPackageMetadata(packageName))
|
||||
assertEquals(updatedMetadata.packageMetadataMap[packageName], manager.getPackageMetadata(packageName))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -181,7 +182,8 @@ class MetadataManagerTest {
|
|||
assertEquals(initialMetadata.token, manager.getBackupToken())
|
||||
}
|
||||
|
||||
private fun expectWriteToCache(metadata: BackupMetadata) {
|
||||
private fun expectModifyMetadata(metadata: BackupMetadata) {
|
||||
every { metadataWriter.write(metadata, storageOutputStream) } just Runs
|
||||
every { metadataWriter.encode(metadata) } returns encodedMetadata
|
||||
every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream
|
||||
every { cacheOutputStream.write(encodedMetadata) } just Runs
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.stevesoltys.seedvault.metadata
|
|||
import com.stevesoltys.seedvault.Utf8
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
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
|
||||
|
@ -82,6 +84,7 @@ class MetadataReaderTest {
|
|||
val packageMetadata = HashMap<String, PackageMetadata>().apply {
|
||||
put("org.example", PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
state = QUOTA_EXCEEDED,
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
|
@ -108,6 +111,22 @@ 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_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(UNKNOWN_ERROR, metadata.packageMetadataMap["org.example"]!!.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `package metadata can only include time`() {
|
||||
val json = JSONObject(metadataByteArray.toString(Utf8))
|
||||
|
@ -124,7 +143,7 @@ class MetadataReaderTest {
|
|||
assertNull(packageMetadata.signatures)
|
||||
}
|
||||
|
||||
private fun getMetadata(packageMetadata: HashMap<String, PackageMetadata> = HashMap()): BackupMetadata {
|
||||
private fun getMetadata(packageMetadata: PackageMetadataMap = PackageMetadataMap()): BackupMetadata {
|
||||
return BackupMetadata(
|
||||
version = 1.toByte(),
|
||||
token = Random.nextLong(),
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.metadata
|
|||
|
||||
import com.stevesoltys.seedvault.crypto.Crypto
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -27,7 +28,7 @@ internal class MetadataWriterDecoderTest {
|
|||
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))
|
||||
put(getRandomString(), PackageMetadata(time, APK_AND_DATA))
|
||||
}
|
||||
val metadata = getMetadata(packages)
|
||||
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
||||
|
@ -38,6 +39,7 @@ internal class MetadataWriterDecoderTest {
|
|||
val packages = HashMap<String, PackageMetadata>().apply {
|
||||
put(getRandomString(), PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
state = APK_AND_DATA,
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
|
@ -52,6 +54,7 @@ internal class MetadataWriterDecoderTest {
|
|||
val packages = HashMap<String, PackageMetadata>().apply {
|
||||
put(getRandomString(), PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
state = QUOTA_EXCEEDED,
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
|
@ -59,6 +62,7 @@ internal class MetadataWriterDecoderTest {
|
|||
))
|
||||
put(getRandomString(), PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
state = NO_DATA,
|
||||
version = Random.nextLong(),
|
||||
installer = getRandomString(),
|
||||
sha256 = getRandomString(),
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
|
|||
import com.stevesoltys.seedvault.header.HeaderWriterImpl
|
||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
||||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.transport.backup.*
|
||||
import com.stevesoltys.seedvault.transport.restore.*
|
||||
import io.mockk.*
|
||||
|
@ -23,7 +24,6 @@ import org.junit.jupiter.api.Assertions.*
|
|||
import org.junit.jupiter.api.Test
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.OutputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class CoordinatorIntegrationTest : TransportTest() {
|
||||
|
@ -59,6 +59,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
|
||||
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
|
||||
private val metadataOutputStream = ByteArrayOutputStream()
|
||||
private val packageMetadata = PackageMetadata(time = 0L)
|
||||
private val key = "RestoreKey"
|
||||
private val key64 = key.encodeBase64()
|
||||
private val key2 = "RestoreKey2"
|
||||
|
@ -93,8 +94,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
appData2.size
|
||||
}
|
||||
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns true
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata
|
||||
every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||
|
||||
// start and finish K/V backup
|
||||
|
@ -146,7 +148,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
appData.size
|
||||
}
|
||||
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns false
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns null
|
||||
every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||
|
||||
|
@ -184,8 +186,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
|||
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
||||
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
||||
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns true
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata
|
||||
every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||
|
||||
// perform backup to output stream
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.util.PackageUtils
|
|||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.*
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -26,7 +27,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
private val pm: PackageManager = mockk()
|
||||
private val streamGetter: () -> OutputStream = mockk()
|
||||
|
||||
private val apkBackup = ApkBackup(pm, clock, settingsManager, metadataManager)
|
||||
private val apkBackup = ApkBackup(pm, settingsManager, metadataManager)
|
||||
|
||||
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
||||
|
@ -44,14 +45,14 @@ internal class ApkBackupTest : BackupTest() {
|
|||
@Test
|
||||
fun `does not back up @pm@`() {
|
||||
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
||||
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not back up when setting disabled`() {
|
||||
every { settingsManager.backupApks() } returns false
|
||||
|
||||
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -60,7 +61,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
|
||||
every { settingsManager.backupApks() } returns true
|
||||
|
||||
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -72,7 +73,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
|
||||
expectChecks(packageMetadata)
|
||||
|
||||
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -82,7 +83,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
expectChecks()
|
||||
|
||||
assertThrows(IOException::class.java) {
|
||||
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,7 +94,7 @@ internal class ApkBackupTest : BackupTest() {
|
|||
every { sigInfo.hasMultipleSigners() } returns false
|
||||
every { sigInfo.signingCertificateHistory } returns emptyArray()
|
||||
|
||||
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -106,7 +107,8 @@ internal class ApkBackupTest : BackupTest() {
|
|||
}.absolutePath
|
||||
val apkOutputStream = ByteArrayOutputStream()
|
||||
val updatedMetadata = PackageMetadata(
|
||||
time = Random.nextLong(),
|
||||
time = 0L,
|
||||
state = UNKNOWN_ERROR,
|
||||
version = packageInfo.longVersionCode,
|
||||
installer = getRandomString(),
|
||||
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
||||
|
@ -116,10 +118,9 @@ internal class ApkBackupTest : BackupTest() {
|
|||
expectChecks()
|
||||
every { streamGetter.invoke() } returns apkOutputStream
|
||||
every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer
|
||||
every { clock.time() } returns updatedMetadata.time
|
||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata) } just Runs
|
||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata, outputStream) } just Runs
|
||||
|
||||
assertTrue(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertEquals(updatedMetadata, apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
||||
assertArrayEquals(apkBytes, apkOutputStream.toByteArray())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package com.stevesoltys.seedvault.transport.backup
|
||||
|
||||
import android.app.backup.BackupTransport.TRANSPORT_ERROR
|
||||
import android.app.backup.BackupTransport.TRANSPORT_OK
|
||||
import android.app.backup.BackupTransport.*
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||
import com.stevesoltys.seedvault.getRandomString
|
||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
||||
import com.stevesoltys.seedvault.settings.Storage
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -18,7 +18,7 @@ import java.io.IOException
|
|||
import java.io.OutputStream
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class BackupCoordinatorTest: BackupTest() {
|
||||
internal class BackupCoordinatorTest : BackupTest() {
|
||||
|
||||
private val plugin = mockk<BackupPlugin>()
|
||||
private val kv = mockk<KVBackup>()
|
||||
|
@ -29,6 +29,9 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, metadataManager, settingsManager, notificationManager)
|
||||
|
||||
private val metadataOutputStream = mockk<OutputStream>()
|
||||
private val fileDescriptor: ParcelFileDescriptor = mockk()
|
||||
private val packageMetadata: PackageMetadata = mockk()
|
||||
private val storage = Storage(Uri.EMPTY, getRandomString(), false)
|
||||
|
||||
@Test
|
||||
fun `device initialization succeeds and delegates to plugin`() {
|
||||
|
@ -56,8 +59,6 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
|
||||
@Test
|
||||
fun `error notification when device initialization fails`() {
|
||||
val storage = Storage(Uri.EMPTY, getRandomString(), false)
|
||||
|
||||
every { clock.time() } returns token
|
||||
every { plugin.initializeDevice(token) } throws IOException()
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
|
@ -143,7 +144,8 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
every { kv.hasState() } returns true
|
||||
every { full.hasState() } returns false
|
||||
every { kv.getCurrentPackage() } returns packageInfo
|
||||
expectApkBackupAndMetadataWrite()
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||
every { kv.finishBackup() } returns result
|
||||
|
||||
assertEquals(result, backup.finishBackup())
|
||||
|
@ -156,16 +158,74 @@ internal class BackupCoordinatorTest: BackupTest() {
|
|||
every { kv.hasState() } returns false
|
||||
every { full.hasState() } returns true
|
||||
every { full.getCurrentPackage() } returns packageInfo
|
||||
expectApkBackupAndMetadataWrite()
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||
every { full.finishBackup() } returns result
|
||||
|
||||
assertEquals(result, backup.finishBackup())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `metadata does not get updated when no APK was backed up`() {
|
||||
every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns null
|
||||
|
||||
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `app exceeding quota gets cancelled and reason written to metadata`() {
|
||||
every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
||||
expectApkBackupAndMetadataWrite()
|
||||
every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
||||
every { full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) } returns TRANSPORT_QUOTA_EXCEEDED
|
||||
every { full.getCurrentPackage() } returns packageInfo
|
||||
every { metadataManager.onPackageBackupError(packageInfo.packageName, QUOTA_EXCEEDED, metadataOutputStream) } just Runs
|
||||
every { full.cancelFullBackup() } just Runs
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
|
||||
assertEquals(TRANSPORT_OK,
|
||||
backup.performFullBackup(packageInfo, fileDescriptor, 0))
|
||||
assertEquals(DEFAULT_QUOTA_FULL_BACKUP,
|
||||
backup.getBackupQuota(packageInfo.packageName, true))
|
||||
assertEquals(TRANSPORT_QUOTA_EXCEEDED,
|
||||
backup.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1))
|
||||
backup.cancelFullBackup()
|
||||
assertEquals(0L, backup.requestFullBackupTime())
|
||||
|
||||
verify(exactly = 1) {
|
||||
metadataManager.onPackageBackupError(packageInfo.packageName, QUOTA_EXCEEDED, metadataOutputStream)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `app with no data gets cancelled and reason written to metadata`() {
|
||||
every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
||||
expectApkBackupAndMetadataWrite()
|
||||
every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
||||
every { full.checkFullBackupSize(0) } returns TRANSPORT_PACKAGE_REJECTED
|
||||
every { full.getCurrentPackage() } returns packageInfo
|
||||
every { metadataManager.onPackageBackupError(packageInfo.packageName, NO_DATA, metadataOutputStream) } just Runs
|
||||
every { full.cancelFullBackup() } just Runs
|
||||
every { settingsManager.getStorage() } returns storage
|
||||
|
||||
assertEquals(TRANSPORT_OK,
|
||||
backup.performFullBackup(packageInfo, fileDescriptor, 0))
|
||||
assertEquals(DEFAULT_QUOTA_FULL_BACKUP,
|
||||
backup.getBackupQuota(packageInfo.packageName, true))
|
||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, backup.checkFullBackupSize(0))
|
||||
backup.cancelFullBackup()
|
||||
assertEquals(0L, backup.requestFullBackupTime())
|
||||
|
||||
verify(exactly = 1) {
|
||||
metadataManager.onPackageBackupError(packageInfo.packageName, NO_DATA, metadataOutputStream)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expectApkBackupAndMetadataWrite() {
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns true
|
||||
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata
|
||||
every { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue