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:
Torsten Grote 2020-01-13 12:47:27 -03:00
parent 3f73119b52
commit 3d296e1335
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
14 changed files with 312 additions and 90 deletions

View file

@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.metadata
import android.os.Build import android.os.Build
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import java.io.InputStream import java.io.InputStream
typealias PackageMetadataMap = HashMap<String, PackageMetadata> 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_INCREMENTAL = "incremental"
internal const val JSON_METADATA_NAME = "name" 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( 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 version: Long? = null,
internal val installer: String? = null, internal val installer: String? = null,
internal val sha256: 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_TIME = "time"
internal const val JSON_PACKAGE_STATE = "state"
internal const val JSON_PACKAGE_VERSION = "version" internal const val JSON_PACKAGE_VERSION = "version"
internal const val JSON_PACKAGE_INSTALLER = "installer" internal const val JSON_PACKAGE_INSTALLER = "installer"
internal const val JSON_PACKAGE_SHA256 = "sha256" internal const val JSON_PACKAGE_SHA256 = "sha256"

View file

@ -6,6 +6,7 @@ import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
@ -44,22 +45,21 @@ class MetadataManager(
@Synchronized @Synchronized
@Throws(IOException::class) @Throws(IOException::class)
fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) { fun onDeviceInitialization(token: Long, metadataOutputStream: OutputStream) {
metadata = BackupMetadata(token = token) modifyMetadata(metadataOutputStream) {
metadataWriter.write(metadata, metadataOutputStream) metadata = BackupMetadata(token = token)
writeMetadataToCache() }
} }
/** /**
* Call this after an APK as been successfully written to backup storage. * Call this after a package's APK has been backed up successfully.
* 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. * It updates the packages' metadata
* and writes it encrypted to the given [OutputStream] as well as the internal cache.
*/ */
@Synchronized @Synchronized
fun onApkBackedUp(packageName: String, packageMetadata: PackageMetadata) { @Throws(IOException::class)
fun onApkBackedUp(packageName: String, packageMetadata: PackageMetadata, metadataOutputStream: OutputStream) {
metadata.packageMetadataMap[packageName]?.let { metadata.packageMetadataMap[packageName]?.let {
check(it.time <= packageMetadata.time) {
"APK backup set time of $packageName backwards"
}
check(packageMetadata.version != null) { check(packageMetadata.version != null) {
"APK backup returned 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}" "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 @Synchronized
@Throws(IOException::class) @Throws(IOException::class)
fun onPackageBackedUp(packageName: String, metadataOutputStream: OutputStream) { fun onPackageBackedUp(packageName: String, metadataOutputStream: OutputStream) {
val oldMetadata = metadata.copy() modifyMetadata(metadataOutputStream) {
val now = clock.time() val now = clock.time()
metadata.time = now metadata.time = now
if (metadata.packageMetadataMap.containsKey(packageName)) { if (metadata.packageMetadataMap.containsKey(packageName)) {
metadata.packageMetadataMap[packageName]?.time = now metadata.packageMetadataMap[packageName]!!.time = now
} else { metadata.packageMetadataMap[packageName]!!.state = APK_AND_DATA
metadata.packageMetadataMap[packageName] = PackageMetadata(time = now) } 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 { try {
modFun.invoke()
metadataWriter.write(metadata, metadataOutputStream) metadataWriter.write(metadata, metadataOutputStream)
writeMetadataToCache()
} catch (e: IOException) { } catch (e: IOException) {
Log.w(TAG, "Error writing metadata to storage", e) Log.w(TAG, "Error writing metadata to storage", e)
// revert metadata and do not write it to cache // revert metadata and do not write it to cache
// TODO also revert changes made by last [onApkBackedUp]
metadata = oldMetadata metadata = oldMetadata
throw IOException(e) throw IOException(e)
} }
writeMetadataToCache()
} }
/** /**

View file

@ -4,6 +4,7 @@ import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.UnsupportedVersionException
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.*
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -59,8 +60,14 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
for (packageName in json.keys()) { for (packageName in json.keys()) {
if (packageName == JSON_METADATA) continue if (packageName == JSON_METADATA) continue
val p = json.getJSONObject(packageName) 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 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 pSha256 = p.optString(JSON_PACKAGE_SHA256)
val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES) val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES)
val signatures = if (pSignatures == null) null else val signatures = if (pSignatures == null) null else
@ -71,6 +78,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
} }
packageMetadataMap[packageName] = PackageMetadata( packageMetadataMap[packageName] = PackageMetadata(
time = p.getLong(JSON_PACKAGE_TIME), time = p.getLong(JSON_PACKAGE_TIME),
state = pState,
version = if (pVersion == 0L) null else pVersion, version = if (pVersion == 0L) null else pVersion,
installer = if (pInstaller == "") null else pInstaller, installer = if (pInstaller == "") null else pInstaller,
sha256 = if (pSha256 == "") null else pSha256, sha256 = if (pSha256 == "") null else pSha256,

View file

@ -2,6 +2,7 @@ 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.crypto.Crypto
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -36,6 +37,9 @@ internal class MetadataWriterImpl(private val crypto: Crypto) : MetadataWriter {
for ((packageName, packageMetadata) in metadata.packageMetadataMap) { for ((packageName, packageMetadata) in metadata.packageMetadataMap) {
json.put(packageName, JSONObject().apply { json.put(packageName, JSONObject().apply {
put(JSON_PACKAGE_TIME, packageMetadata.time) 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.version?.let { put(JSON_PACKAGE_VERSION, it) }
packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) } packageMetadata.installer?.let { put(JSON_PACKAGE_INSTALLER, it) }
packageMetadata.sha256?.let { put(JSON_PACKAGE_SHA256, it) } packageMetadata.sha256?.let { put(JSON_PACKAGE_SHA256, it) }

View file

@ -8,7 +8,6 @@ import android.content.pm.Signature
import android.content.pm.SigningInfo import android.content.pm.SigningInfo
import android.util.Log import android.util.Log
import android.util.PackageUtils.computeSha256DigestBytes import android.util.PackageUtils.computeSha256DigestBytes
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
@ -24,43 +23,50 @@ private val TAG = ApkBackup::class.java.simpleName
class ApkBackup( class ApkBackup(
private val pm: PackageManager, private val pm: PackageManager,
private val clock: Clock,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager) { 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) @Throws(IOException::class)
fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): Boolean { fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): PackageMetadata? {
// do not back up @pm@ // do not back up @pm@
val packageName = packageInfo.packageName 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 // 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 // do not back up system apps that haven't been updated
val isSystemApp = packageInfo.applicationInfo.flags and FLAG_SYSTEM != 0 val isSystemApp = packageInfo.applicationInfo.flags and FLAG_SYSTEM != 0
val isUpdatedSystemApp = packageInfo.applicationInfo.flags and FLAG_UPDATED_SYSTEM_APP != 0 val isUpdatedSystemApp = packageInfo.applicationInfo.flags and FLAG_UPDATED_SYSTEM_APP != 0
if (isSystemApp && !isUpdatedSystemApp) { if (isSystemApp && !isUpdatedSystemApp) {
Log.d(TAG, "Package $packageName is vanilla system app. Not backing it up.") 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 // TODO remove when adding support for packages with multiple signers
if (packageInfo.signingInfo.hasMultipleSigners()) { if (packageInfo.signingInfo.hasMultipleSigners()) {
Log.e(TAG, "Package $packageName has multiple signers. Not backing it up.") Log.e(TAG, "Package $packageName has multiple signers. Not backing it up.")
return false return null
} }
// get signatures // get signatures
val signatures = packageInfo.signingInfo.getSignatures() val signatures = packageInfo.signingInfo.getSignatures()
if (signatures.isEmpty()) { if (signatures.isEmpty()) {
Log.e(TAG, "Package $packageName has no signatures. Not backing it up.") Log.e(TAG, "Package $packageName has no signatures. Not backing it up.")
return false return null
} }
// get cached metadata about package // get cached metadata about package
val packageMetadata = metadataManager.getPackageMetadata(packageName) val packageMetadata = metadataManager.getPackageMetadata(packageName)
?: PackageMetadata(time = clock.time()) ?: PackageMetadata()
// get version codes // get version codes
val version = packageInfo.longVersionCode val version = packageInfo.longVersionCode
@ -69,7 +75,7 @@ class ApkBackup(
// do not backup if we have the version already and signatures did not change // do not backup if we have the version already and signatures did not change
if (version <= backedUpVersion && !signaturesChanged(packageMetadata, signatures)) { 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.") 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 // get an InputStream for the APK
@ -100,17 +106,13 @@ class ApkBackup(
val sha256 = messageDigest.digest().encodeBase64() val sha256 = messageDigest.digest().encodeBase64()
Log.d(TAG, "Backed up new APK of $packageName with version $version.") Log.d(TAG, "Backed up new APK of $packageName with version $version.")
// update the metadata // return updated metadata
val installer = pm.getInstallerPackageName(packageName) return PackageMetadata(
val updatedMetadata = PackageMetadata(
time = clock.time(),
version = version, version = version,
installer = installer, installer = pm.getInstallerPackageName(packageName),
sha256 = sha256, sha256 = sha256,
signatures = signatures signatures = signatures
) )
metadataManager.onApkBackedUp(packageName, updatedMetadata)
return true
} }
private fun signaturesChanged(packageMetadata: PackageMetadata, signatures: List<String>): Boolean { private fun signaturesChanged(packageMetadata: PackageMetadata, signatures: List<String>): Boolean {

View file

@ -9,6 +9,8 @@ import com.stevesoltys.seedvault.BackupNotificationManager
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.MetadataManager 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 com.stevesoltys.seedvault.settings.SettingsManager
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit.DAYS import java.util.concurrent.TimeUnit.DAYS
@ -32,6 +34,7 @@ internal class BackupCoordinator(
private var calledInitialize = false private var calledInitialize = false
private var calledClearBackupData = false private var calledClearBackupData = false
private var cancelReason: PackageState = UNKNOWN_ERROR
// ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------
// Transport initialization and quota // Transport initialization and quota
@ -110,13 +113,15 @@ internal class BackupCoordinator(
} }
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
cancelReason = UNKNOWN_ERROR
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
// backups of package manager metadata do not respect backoff // 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 // we need to reject them manually when now is not a good time for a backup
if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) { if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
return TRANSPORT_PACKAGE_REJECTED 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") 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 { 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) 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]. * If the transport receives this callback, it will *not* receive a call to [finishBackup].
* It needs to tear down any ongoing backup state here. * 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 // Clear and Finish
@ -193,6 +211,14 @@ internal class BackupCoordinator(
return TRANSPORT_OK 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 { fun finishBackup(): Int = when {
kv.hasState() -> { kv.hasState() -> {
check(!full.hasState()) { "K/V backup has state, but full backup has dangling state as well" } 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()") 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) { private fun onPackageBackedUp(packageInfo: PackageInfo) {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
try { try {
apkBackup.backupApkIfNecessary(packageInfo) { plugin.getApkOutputStream(packageInfo) }
val outputStream = plugin.getMetadataOutputStream() val outputStream = plugin.getMetadataOutputStream()
metadataManager.onPackageBackedUp(packageName, outputStream) metadataManager.onPackageBackedUp(packageName, outputStream)
} catch (e: IOException) { } 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 { private fun getBackupBackoff(): Long {
val noBackoff = 0L val noBackoff = 0L
val defaultBackoff = DAYS.toMillis(30) val defaultBackoff = DAYS.toMillis(30)

View file

@ -5,7 +5,7 @@ import org.koin.dsl.module
val backupModule = module { val backupModule = module {
single { InputFactory() } single { InputFactory() }
single { ApkBackup(androidContext().packageManager, get(), get(), get()) } single { ApkBackup(androidContext().packageManager, get(), get()) }
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) } single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
single { FullBackup(get<BackupPlugin>().fullBackupPlugin, get(), get(), get()) } single { FullBackup(get<BackupPlugin>().fullBackupPlugin, get(), get(), get()) }
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) } single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) }

View file

@ -45,7 +45,7 @@ internal class FullBackup(
Log.i(TAG, "Check full backup size of $size bytes.") Log.i(TAG, "Check full backup size of $size bytes.")
return when { return when {
size <= 0 -> TRANSPORT_PACKAGE_REJECTED size <= 0 -> TRANSPORT_PACKAGE_REJECTED
size > plugin.getQuota() -> TRANSPORT_QUOTA_EXCEEDED size > getQuota() -> TRANSPORT_QUOTA_EXCEEDED
else -> TRANSPORT_OK else -> TRANSPORT_OK
} }
} }

View file

@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
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 io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
@ -46,8 +47,8 @@ class MetadataManagerTest {
@Test @Test
fun `test onDeviceInitialization()`() { fun `test onDeviceInitialization()`() {
every { clock.time() } returns time every { clock.time() } returns time
every { metadataWriter.write(initialMetadata, storageOutputStream) } just Runs expectReadFromCache()
expectWriteToCache(initialMetadata) expectModifyMetadata(initialMetadata)
manager.onDeviceInitialization(token, storageOutputStream) manager.onDeviceInitialization(token, storageOutputStream)
@ -58,15 +59,16 @@ class MetadataManagerTest {
@Test @Test
fun `test onApkBackedUp() with no prior package metadata`() { fun `test onApkBackedUp() with no prior package metadata`() {
val packageMetadata = PackageMetadata( val packageMetadata = PackageMetadata(
time = time + 1, time = 0L,
version = Random.nextLong(Long.MAX_VALUE), version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(), installer = getRandomString(),
signatures = listOf("sig") signatures = listOf("sig")
) )
expectReadFromCache() expectReadFromCache()
expectModifyMetadata(initialMetadata)
manager.onApkBackedUp(packageName, packageMetadata) manager.onApkBackedUp(packageName, packageMetadata, storageOutputStream)
assertEquals(packageMetadata, manager.getPackageMetadata(packageName)) assertEquals(packageMetadata, manager.getPackageMetadata(packageName))
} }
@ -81,15 +83,16 @@ class MetadataManagerTest {
) )
initialMetadata.packageMetadataMap[packageName] = packageMetadata initialMetadata.packageMetadataMap[packageName] = packageMetadata
val updatedPackageMetadata = PackageMetadata( val updatedPackageMetadata = PackageMetadata(
time = time + 1, time = time,
version = packageMetadata.version!! + 1, version = packageMetadata.version!! + 1,
installer = getRandomString(), installer = getRandomString(),
signatures = listOf("sig foo") signatures = listOf("sig foo")
) )
expectReadFromCache() expectReadFromCache()
expectModifyMetadata(initialMetadata)
manager.onApkBackedUp(packageName, updatedPackageMetadata) manager.onApkBackedUp(packageName, updatedPackageMetadata, storageOutputStream)
assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName)) assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName))
} }
@ -102,8 +105,7 @@ class MetadataManagerTest {
expectReadFromCache() expectReadFromCache()
every { clock.time() } returns time every { clock.time() } returns time
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs expectModifyMetadata(updatedMetadata)
expectWriteToCache(updatedMetadata)
manager.onPackageBackedUp(packageName, storageOutputStream) manager.onPackageBackedUp(packageName, storageOutputStream)
@ -142,19 +144,18 @@ class MetadataManagerTest {
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime) cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(cacheTime)
val updatedMetadata = cachedMetadata.copy(time = time) val updatedMetadata = cachedMetadata.copy(time = time)
cachedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time) updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
cachedMetadata.packageMetadataMap[packageName] = PackageMetadata(time) updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(time, state = APK_AND_DATA)
expectReadFromCache() expectReadFromCache()
every { clock.time() } returns time every { clock.time() } returns time
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs expectModifyMetadata(updatedMetadata)
expectWriteToCache(updatedMetadata)
manager.onPackageBackedUp(packageName, storageOutputStream) manager.onPackageBackedUp(packageName, storageOutputStream)
assertEquals(time, manager.getLastBackupTime()) assertEquals(time, manager.getLastBackupTime())
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName)) assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
assertEquals(PackageMetadata(time), manager.getPackageMetadata(packageName)) assertEquals(updatedMetadata.packageMetadataMap[packageName], manager.getPackageMetadata(packageName))
} }
@Test @Test
@ -181,7 +182,8 @@ class MetadataManagerTest {
assertEquals(initialMetadata.token, manager.getBackupToken()) 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 { metadataWriter.encode(metadata) } returns encodedMetadata
every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream
every { cacheOutputStream.write(encodedMetadata) } just Runs every { cacheOutputStream.write(encodedMetadata) } just Runs

View file

@ -3,6 +3,8 @@ 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.crypto.Crypto
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
@ -82,6 +84,7 @@ class MetadataReaderTest {
val packageMetadata = HashMap<String, PackageMetadata>().apply { val packageMetadata = HashMap<String, PackageMetadata>().apply {
put("org.example", PackageMetadata( put("org.example", PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = QUOTA_EXCEEDED,
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
sha256 = 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 @Test
fun `package metadata can only include time`() { fun `package metadata can only include time`() {
val json = JSONObject(metadataByteArray.toString(Utf8)) val json = JSONObject(metadataByteArray.toString(Utf8))
@ -124,7 +143,7 @@ class MetadataReaderTest {
assertNull(packageMetadata.signatures) assertNull(packageMetadata.signatures)
} }
private fun getMetadata(packageMetadata: HashMap<String, PackageMetadata> = HashMap()): BackupMetadata { private fun getMetadata(packageMetadata: PackageMetadataMap = PackageMetadataMap()): BackupMetadata {
return BackupMetadata( return BackupMetadata(
version = 1.toByte(), version = 1.toByte(),
token = Random.nextLong(), token = Random.nextLong(),

View file

@ -2,6 +2,7 @@ package com.stevesoltys.seedvault.metadata
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageState.*
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -27,7 +28,7 @@ internal class MetadataWriterDecoderTest {
fun `encoded metadata matches decoded metadata (with package, no apk info)`() { fun `encoded metadata matches decoded metadata (with package, no apk info)`() {
val time = Random.nextLong() val time = Random.nextLong()
val packages = HashMap<String, PackageMetadata>().apply { val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(time)) put(getRandomString(), PackageMetadata(time, APK_AND_DATA))
} }
val metadata = getMetadata(packages) val metadata = getMetadata(packages)
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)) assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
@ -38,6 +39,7 @@ internal class MetadataWriterDecoderTest {
val packages = HashMap<String, PackageMetadata>().apply { val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata( put(getRandomString(), PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = APK_AND_DATA,
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
sha256 = getRandomString(), sha256 = getRandomString(),
@ -52,6 +54,7 @@ internal class MetadataWriterDecoderTest {
val packages = HashMap<String, PackageMetadata>().apply { val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata( put(getRandomString(), PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = QUOTA_EXCEEDED,
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
sha256 = getRandomString(), sha256 = getRandomString(),
@ -59,6 +62,7 @@ internal class MetadataWriterDecoderTest {
)) ))
put(getRandomString(), PackageMetadata( put(getRandomString(), PackageMetadata(
time = Random.nextLong(), time = Random.nextLong(),
state = NO_DATA,
version = Random.nextLong(), version = Random.nextLong(),
installer = getRandomString(), installer = getRandomString(),
sha256 = getRandomString(), sha256 = getRandomString(),

View file

@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.header.HeaderReaderImpl
import com.stevesoltys.seedvault.header.HeaderWriterImpl import com.stevesoltys.seedvault.header.HeaderWriterImpl
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.transport.backup.* import com.stevesoltys.seedvault.transport.backup.*
import com.stevesoltys.seedvault.transport.restore.* import com.stevesoltys.seedvault.transport.restore.*
import io.mockk.* import io.mockk.*
@ -23,7 +24,6 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.OutputStream
import kotlin.random.Random import kotlin.random.Random
internal class CoordinatorIntegrationTest : TransportTest() { internal class CoordinatorIntegrationTest : TransportTest() {
@ -59,6 +59,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val appData = ByteArray(42).apply { Random.nextBytes(this) } private val appData = ByteArray(42).apply { Random.nextBytes(this) }
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) } private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
private val metadataOutputStream = ByteArrayOutputStream() private val metadataOutputStream = ByteArrayOutputStream()
private val packageMetadata = PackageMetadata(time = 0L)
private val key = "RestoreKey" private val key = "RestoreKey"
private val key64 = key.encodeBase64() private val key64 = key.encodeBase64()
private val key2 = "RestoreKey2" private val key2 = "RestoreKey2"
@ -93,8 +94,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData2.size appData2.size
} }
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2 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 { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
// start and finish K/V backup // start and finish K/V backup
@ -146,7 +148,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData.size appData.size
} }
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream 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 { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
@ -184,8 +186,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP 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 { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
// perform backup to output stream // perform backup to output stream

View file

@ -9,6 +9,7 @@ import android.util.PackageUtils
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -26,7 +27,7 @@ internal class ApkBackupTest : BackupTest() {
private val pm: PackageManager = mockk() private val pm: PackageManager = mockk()
private val streamGetter: () -> OutputStream = 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 signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01) private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
@ -44,14 +45,14 @@ internal class ApkBackupTest : BackupTest() {
@Test @Test
fun `does not back up @pm@`() { fun `does not back up @pm@`() {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
} }
@Test @Test
fun `does not back up when setting disabled`() { fun `does not back up when setting disabled`() {
every { settingsManager.backupApks() } returns false every { settingsManager.backupApks() } returns false
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
} }
@Test @Test
@ -60,7 +61,7 @@ internal class ApkBackupTest : BackupTest() {
every { settingsManager.backupApks() } returns true every { settingsManager.backupApks() } returns true
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
} }
@Test @Test
@ -72,7 +73,7 @@ internal class ApkBackupTest : BackupTest() {
expectChecks(packageMetadata) expectChecks(packageMetadata)
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
} }
@Test @Test
@ -82,7 +83,7 @@ internal class ApkBackupTest : BackupTest() {
expectChecks() expectChecks()
assertThrows(IOException::class.java) { 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.hasMultipleSigners() } returns false
every { sigInfo.signingCertificateHistory } returns emptyArray() every { sigInfo.signingCertificateHistory } returns emptyArray()
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
} }
@Test @Test
@ -106,7 +107,8 @@ internal class ApkBackupTest : BackupTest() {
}.absolutePath }.absolutePath
val apkOutputStream = ByteArrayOutputStream() val apkOutputStream = ByteArrayOutputStream()
val updatedMetadata = PackageMetadata( val updatedMetadata = PackageMetadata(
time = Random.nextLong(), time = 0L,
state = UNKNOWN_ERROR,
version = packageInfo.longVersionCode, version = packageInfo.longVersionCode,
installer = getRandomString(), installer = getRandomString(),
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI", sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
@ -116,10 +118,9 @@ internal class ApkBackupTest : BackupTest() {
expectChecks() expectChecks()
every { streamGetter.invoke() } returns apkOutputStream every { streamGetter.invoke() } returns apkOutputStream
every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer
every { clock.time() } returns updatedMetadata.time every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata, outputStream) } just Runs
every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata) } just Runs
assertTrue(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) assertEquals(updatedMetadata, apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
assertArrayEquals(apkBytes, apkOutputStream.toByteArray()) assertArrayEquals(apkBytes, apkOutputStream.toByteArray())
} }

View file

@ -1,16 +1,16 @@
package com.stevesoltys.seedvault.transport.backup package com.stevesoltys.seedvault.transport.backup
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.*
import android.app.backup.BackupTransport.TRANSPORT_OK
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.BackupNotificationManager
import com.stevesoltys.seedvault.getRandomString 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 com.stevesoltys.seedvault.settings.Storage
import io.mockk.Runs import io.mockk.*
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -18,7 +18,7 @@ import java.io.IOException
import java.io.OutputStream import java.io.OutputStream
import kotlin.random.Random import kotlin.random.Random
internal class BackupCoordinatorTest: BackupTest() { internal class BackupCoordinatorTest : BackupTest() {
private val plugin = mockk<BackupPlugin>() private val plugin = mockk<BackupPlugin>()
private val kv = mockk<KVBackup>() 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 backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, metadataManager, settingsManager, notificationManager)
private val metadataOutputStream = mockk<OutputStream>() private val metadataOutputStream = mockk<OutputStream>()
private val fileDescriptor: ParcelFileDescriptor = mockk()
private val packageMetadata: PackageMetadata = mockk()
private val storage = Storage(Uri.EMPTY, getRandomString(), false)
@Test @Test
fun `device initialization succeeds and delegates to plugin`() { fun `device initialization succeeds and delegates to plugin`() {
@ -56,8 +59,6 @@ internal class BackupCoordinatorTest: BackupTest() {
@Test @Test
fun `error notification when device initialization fails`() { fun `error notification when device initialization fails`() {
val storage = Storage(Uri.EMPTY, getRandomString(), false)
every { clock.time() } returns token every { clock.time() } returns token
every { plugin.initializeDevice(token) } throws IOException() every { plugin.initializeDevice(token) } throws IOException()
every { settingsManager.getStorage() } returns storage every { settingsManager.getStorage() } returns storage
@ -143,7 +144,8 @@ internal class BackupCoordinatorTest: BackupTest() {
every { kv.hasState() } returns true every { kv.hasState() } returns true
every { full.hasState() } returns false every { full.hasState() } returns false
every { kv.getCurrentPackage() } returns packageInfo every { kv.getCurrentPackage() } returns packageInfo
expectApkBackupAndMetadataWrite() every { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
every { kv.finishBackup() } returns result every { kv.finishBackup() } returns result
assertEquals(result, backup.finishBackup()) assertEquals(result, backup.finishBackup())
@ -156,16 +158,74 @@ internal class BackupCoordinatorTest: BackupTest() {
every { kv.hasState() } returns false every { kv.hasState() } returns false
every { full.hasState() } returns true every { full.hasState() } returns true
every { full.getCurrentPackage() } returns packageInfo every { full.getCurrentPackage() } returns packageInfo
expectApkBackupAndMetadataWrite() every { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
every { full.finishBackup() } returns result every { full.finishBackup() } returns result
assertEquals(result, backup.finishBackup()) 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() { private fun expectApkBackupAndMetadataWrite() {
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns true every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata
every { plugin.getMetadataOutputStream() } returns metadataOutputStream every { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
} }
} }