Replace all instances of DocumentFile#findFile with #findFileBlocking

Also start sticking closer to the official Kotlin formatting style
This commit is contained in:
Torsten Grote 2020-08-12 11:29:40 -03:00 committed by Chirayu Desai
parent 18d83767b3
commit 2958c8fac8
22 changed files with 561 additions and 287 deletions

View file

@ -11,9 +11,11 @@ import java.io.OutputStream
private const val MIME_TYPE_APK = "application/vnd.android.package-archive"
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderBackupPlugin(
private val context: Context,
private val storage: DocumentsStorage) : BackupPlugin {
private val context: Context,
private val storage: DocumentsStorage
) : BackupPlugin {
private val packageManager: PackageManager = context.packageManager
@ -41,7 +43,7 @@ internal class DocumentsProviderBackupPlugin(
val fullDir = storage.currentFullBackupDir
// wipe existing data
storage.getSetDir()?.findFile(FILE_BACKUP_METADATA)?.delete()
storage.getSetDir()?.findFileBlocking(context, FILE_BACKUP_METADATA)?.delete()
kvDir?.deleteContents()
fullDir?.deleteContents()

View file

@ -10,24 +10,27 @@ import java.io.OutputStream
private val TAG = DocumentsProviderFullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderFullBackup(
private val storage: DocumentsStorage,
private val context: Context) : FullBackupPlugin {
private val storage: DocumentsStorage,
private val context: Context
) : FullBackupPlugin {
override fun getQuota() = DEFAULT_QUOTA_FULL_BACKUP
@Throws(IOException::class)
override suspend fun getOutputStream(targetPackage: PackageInfo): OutputStream {
val file = storage.currentFullBackupDir?.createOrGetFile(context, targetPackage.packageName)
?: throw IOException()
?: throw IOException()
return storage.getOutputStream(file)
}
@Throws(IOException::class)
override fun removeDataOfPackage(packageInfo: PackageInfo) {
override suspend fun removeDataOfPackage(packageInfo: PackageInfo) {
val packageName = packageInfo.packageName
Log.i(TAG, "Deleting $packageName...")
val file = storage.currentFullBackupDir?.findFile(packageName) ?: return
val file = storage.currentFullBackupDir?.findFileBlocking(context, packageName)
?: return
if (!file.delete()) throw IOException("Failed to delete $packageName")
}

View file

@ -1,23 +1,31 @@
package com.stevesoltys.seedvault.plugins.saf
import android.content.Context
import android.content.pm.PackageInfo
import com.stevesoltys.seedvault.transport.restore.FullRestorePlugin
import java.io.IOException
import java.io.InputStream
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderFullRestorePlugin(
private val documentsStorage: DocumentsStorage) : FullRestorePlugin {
private val context: Context,
private val documentsStorage: DocumentsStorage
) : FullRestorePlugin {
@Throws(IOException::class)
override suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
val backupDir = documentsStorage.getFullBackupDir(token) ?: return false
return backupDir.findFile(packageInfo.packageName) != null
return backupDir.findFileBlocking(context, packageInfo.packageName) != null
}
@Throws(IOException::class)
override suspend fun getInputStreamForPackage(token: Long, packageInfo: PackageInfo): InputStream {
override suspend fun getInputStreamForPackage(
token: Long,
packageInfo: PackageInfo
): InputStream {
val backupDir = documentsStorage.getFullBackupDir(token) ?: throw IOException()
val packageFile = backupDir.findFile(packageInfo.packageName) ?: throw IOException()
val packageFile =
backupDir.findFileBlocking(context, packageInfo.packageName) ?: throw IOException()
return documentsStorage.getInputStream(packageFile)
}

View file

@ -8,9 +8,10 @@ import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
import java.io.IOException
import java.io.OutputStream
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderKVBackup(
private val storage: DocumentsStorage,
private val context: Context
private val storage: DocumentsStorage,
private val context: Context
) : KVBackupPlugin {
private var packageFile: DocumentFile? = null
@ -18,8 +19,9 @@ internal class DocumentsProviderKVBackup(
override fun getQuota(): Long = DEFAULT_QUOTA_KEY_VALUE_BACKUP
@Throws(IOException::class)
override fun hasDataForPackage(packageInfo: PackageInfo): Boolean {
val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName)
override suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean {
val packageFile =
storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName)
?: return false
return packageFile.listFiles().isNotEmpty()
}
@ -27,27 +29,31 @@ internal class DocumentsProviderKVBackup(
@Throws(IOException::class)
override suspend fun ensureRecordStorageForPackage(packageInfo: PackageInfo) {
// remember package file for subsequent operations
packageFile = storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName)
packageFile =
storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName)
}
@Throws(IOException::class)
override fun removeDataOfPackage(packageInfo: PackageInfo) {
override suspend fun removeDataOfPackage(packageInfo: PackageInfo) {
// we cannot use the cached this.packageFile here,
// because this can be called before [ensureRecordStorageForPackage]
val packageFile = storage.currentKvBackupDir?.findFile(packageInfo.packageName) ?: return
val packageFile = storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) ?: return
packageFile.delete()
}
@Throws(IOException::class)
override fun deleteRecord(packageInfo: PackageInfo, key: String) {
override suspend fun deleteRecord(packageInfo: PackageInfo, key: String) {
val packageFile = this.packageFile ?: throw AssertionError()
packageFile.assertRightFile(packageInfo)
val keyFile = packageFile.findFile(key) ?: return
val keyFile = packageFile.findFileBlocking(context, key) ?: return
keyFile.delete()
}
@Throws(IOException::class)
override suspend fun getOutputStreamForRecord(packageInfo: PackageInfo, key: String): OutputStream {
override suspend fun getOutputStreamForRecord(
packageInfo: PackageInfo,
key: String
): OutputStream {
val packageFile = this.packageFile ?: throw AssertionError()
packageFile.assertRightFile(packageInfo)
val keyFile = packageFile.createOrGetFile(context, key)

View file

@ -1,12 +1,17 @@
package com.stevesoltys.seedvault.plugins.saf
import android.content.Context
import android.content.pm.PackageInfo
import androidx.documentfile.provider.DocumentFile
import com.stevesoltys.seedvault.transport.restore.KVRestorePlugin
import java.io.IOException
import java.io.InputStream
internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsStorage) : KVRestorePlugin {
@Suppress("BlockingMethodInNonBlockingContext")
internal class DocumentsProviderKVRestorePlugin(
private val context: Context,
private val storage: DocumentsStorage
) : KVRestorePlugin {
private var packageDir: DocumentFile? = null
@ -14,7 +19,7 @@ internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsSt
return try {
val backupDir = storage.getKVBackupDir(token) ?: return false
// remember package file for subsequent operations
packageDir = backupDir.findFile(packageInfo.packageName)
packageDir = backupDir.findFileBlocking(context, packageInfo.packageName)
packageDir != null
} catch (e: IOException) {
false
@ -25,15 +30,19 @@ internal class DocumentsProviderKVRestorePlugin(private val storage: DocumentsSt
val packageDir = this.packageDir ?: throw AssertionError()
packageDir.assertRightFile(packageInfo)
return packageDir.listFiles()
.filter { file -> file.name != null }
.map { file -> file.name!! }
.filter { file -> file.name != null }
.map { file -> file.name!! }
}
@Throws(IOException::class)
override fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream {
override suspend fun getInputStreamForRecord(
token: Long,
packageInfo: PackageInfo,
key: String
): InputStream {
val packageDir = this.packageDir ?: throw AssertionError()
packageDir.assertRightFile(packageInfo)
val keyFile = packageDir.findFile(key) ?: throw IOException()
val keyFile = packageDir.findFileBlocking(context, key) ?: throw IOException()
return storage.getInputStream(keyFile)
}

View file

@ -16,17 +16,18 @@ import java.io.InputStream
private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
internal class DocumentsProviderRestorePlugin(
private val context: Context,
private val storage: DocumentsStorage) : RestorePlugin {
private val context: Context,
private val storage: DocumentsStorage
) : RestorePlugin {
override val kvRestorePlugin: KVRestorePlugin by lazy {
DocumentsProviderKVRestorePlugin(storage)
DocumentsProviderKVRestorePlugin(context, storage)
}
override val fullRestorePlugin: FullRestorePlugin by lazy {
DocumentsProviderFullRestorePlugin(storage)
DocumentsProviderFullRestorePlugin(context, storage)
}
@Throws(IOException::class)
@ -42,7 +43,7 @@ internal class DocumentsProviderRestorePlugin(
val backupSets = getBackups(context, rootDir)
val iterator = backupSets.iterator()
return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence
if (!iterator.hasNext()) return@generateSequence null // end sequence
val backupSet = iterator.next()
try {
val stream = storage.getInputStream(backupSet.metadataFile)
@ -64,18 +65,9 @@ internal class DocumentsProviderRestorePlugin(
return backupSets
}
for (set in files) {
if (!set.isDirectory || set.name == null) {
if (set.name != FILE_NO_MEDIA) {
Log.w(TAG, "Found invalid backup set folder: ${set.name}")
}
continue
}
val token = try {
set.name!!.toLong()
} catch (e: NumberFormatException) {
Log.w(TAG, "Found invalid backup set folder: ${set.name}")
continue
}
// get current token from set or continue to next file/set
val token = set.getTokenOrNull() ?: continue
// block until children of set are available
val metadata = try {
set.findFileBlocking(context, FILE_BACKUP_METADATA)
@ -92,10 +84,26 @@ internal class DocumentsProviderRestorePlugin(
return backupSets
}
private fun DocumentFile.getTokenOrNull(): Long? {
if (!isDirectory || name == null) {
if (name != FILE_NO_MEDIA) {
Log.w(TAG, "Found invalid backup set folder: $name")
}
return null
}
return try {
name!!.toLong()
} catch (e: NumberFormatException) {
Log.w(TAG, "Found invalid backup set folder: $name")
null
}
}
@Throws(IOException::class)
override suspend fun getApkInputStream(token: Long, packageName: String): InputStream {
val setDir = storage.getSetDir(token) ?: throw IOException()
val file = setDir.findFile("$packageName.apk") ?: throw FileNotFoundException()
val file =
setDir.findFileBlocking(context, "$packageName.apk") ?: throw FileNotFoundException()
return storage.getInputStream(file)
}

View file

@ -41,9 +41,9 @@ private const val MIME_TYPE = "application/octet-stream"
private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage(
private val context: Context,
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager
private val context: Context,
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager
) {
private val contentResolver = context.contentResolver
@ -58,7 +58,7 @@ internal class DocumentsStorage(
get() = runBlocking {
if (field == null) {
val parent = storage?.getDocumentFile(context)
?: return@runBlocking null
?: return@runBlocking null
field = try {
parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply {
// create .nomedia file to prevent Android's MediaScanner
@ -180,7 +180,11 @@ internal class DocumentsStorage(
* If we were trying to create it right away, some providers create "filename (1)".
*/
@Throws(IOException::class)
internal suspend fun DocumentFile.createOrGetFile(context: Context, name: String, mimeType: String = MIME_TYPE): DocumentFile {
internal suspend fun DocumentFile.createOrGetFile(
context: Context,
name: String,
mimeType: String = MIME_TYPE
): DocumentFile {
return findFileBlocking(context, name) ?: createFile(mimeType, name)?.apply {
check(this.name == name) { "File named ${this.name}, but should be $name" }
} ?: throw IOException()
@ -276,25 +280,26 @@ suspend fun DocumentFile.findFileBlocking(context: Context, displayName: String)
*/
@VisibleForTesting
@Throws(IOException::class, TimeoutCancellationException::class)
internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = withTimeout(timeout) {
suspendCancellableCoroutine<Cursor> { cont ->
val cursor = query() ?: throw IOException()
cont.invokeOnCancellation { closeQuietly(cursor) }
val loading = cursor.extras.getBoolean(EXTRA_LOADING, false)
if (loading) {
Log.d(TAG, "Wait for children to get loaded...")
cursor.registerContentObserver(object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
Log.d(TAG, "Children loaded. Continue...")
closeQuietly(cursor)
val newCursor = query()
if (newCursor == null) cont.cancel(IOException("query returned no results"))
else cont.resume(newCursor)
}
})
} else {
// not loading, return cursor right away
cont.resume(cursor)
internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) =
withTimeout(timeout) {
suspendCancellableCoroutine<Cursor> { cont ->
val cursor = query() ?: throw IOException()
cont.invokeOnCancellation { closeQuietly(cursor) }
val loading = cursor.extras.getBoolean(EXTRA_LOADING, false)
if (loading) {
Log.d(TAG, "Wait for children to get loaded...")
cursor.registerContentObserver(object : ContentObserver(null) {
override fun onChange(selfChange: Boolean, uri: Uri?) {
Log.d(TAG, "Children loaded. Continue...")
closeQuietly(cursor)
val newCursor = query()
if (newCursor == null) cont.cancel(IOException("query returned no results"))
else cont.resume(newCursor)
}
})
} else {
// not loading, return cursor right away
cont.resume(cursor)
}
}
}
}

View file

@ -18,14 +18,16 @@ import org.koin.core.inject
val TRANSPORT_ID: String = ConfigurableBackupTransport::class.java.name
private const val TRANSPORT_DIRECTORY_NAME = "com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
private const val TRANSPORT_DIRECTORY_NAME =
"com.stevesoltys.seedvault.transport.ConfigurableBackupTransport"
private val TAG = ConfigurableBackupTransport::class.java.simpleName
/**
* @author Steve Soltys
* @author Torsten Grote
*/
class ConfigurableBackupTransport internal constructor(private val context: Context) : BackupTransport(), KoinComponent {
class ConfigurableBackupTransport internal constructor(private val context: Context) :
BackupTransport(), KoinComponent {
private val backupCoordinator by inject<BackupCoordinator>()
private val restoreCoordinator by inject<RestoreCoordinator>()
@ -62,7 +64,10 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
backupCoordinator.initializeDevice()
}
override fun isAppEligibleForBackup(targetPackage: PackageInfo, isFullBackup: Boolean): Boolean {
override fun isAppEligibleForBackup(
targetPackage: PackageInfo,
isFullBackup: Boolean
): Boolean {
return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
}
@ -70,8 +75,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
backupCoordinator.getBackupQuota(packageName, isFullBackup)
}
override fun clearBackupData(packageInfo: PackageInfo): Int {
return backupCoordinator.clearBackupData(packageInfo)
override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
backupCoordinator.clearBackupData(packageInfo)
}
override fun finishBackup(): Int = runBlocking {
@ -86,11 +91,18 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
return backupCoordinator.requestBackupTime()
}
override fun performBackup(packageInfo: PackageInfo, inFd: ParcelFileDescriptor, flags: Int): Int = runBlocking {
override fun performBackup(
packageInfo: PackageInfo,
inFd: ParcelFileDescriptor,
flags: Int
): Int = runBlocking {
backupCoordinator.performIncrementalBackup(packageInfo, inFd, flags)
}
override fun performBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int {
override fun performBackup(
targetPackage: PackageInfo,
fileDescriptor: ParcelFileDescriptor
): Int {
Log.w(TAG, "Warning: Legacy performBackup() method called.")
return performBackup(targetPackage, fileDescriptor, 0)
}
@ -107,11 +119,18 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
return backupCoordinator.checkFullBackupSize(size)
}
override fun performFullBackup(targetPackage: PackageInfo, socket: ParcelFileDescriptor, flags: Int): Int = runBlocking {
override fun performFullBackup(
targetPackage: PackageInfo,
socket: ParcelFileDescriptor,
flags: Int
): Int = runBlocking {
backupCoordinator.performFullBackup(targetPackage, socket, flags)
}
override fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor): Int = runBlocking {
override fun performFullBackup(
targetPackage: PackageInfo,
fileDescriptor: ParcelFileDescriptor
): Int = runBlocking {
Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0)
}
@ -148,8 +167,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
restoreCoordinator.nextRestorePackage()
}
override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int {
return restoreCoordinator.getRestoreData(outputFileDescriptor)
override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int = runBlocking {
restoreCoordinator.getRestoreData(outputFileDescriptor)
}
override fun abortFullRestore(): Int {

View file

@ -33,16 +33,17 @@ private val TAG = BackupCoordinator::class.java.simpleName
@WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
@Suppress("BlockingMethodInNonBlockingContext")
internal class BackupCoordinator(
private val context: Context,
private val plugin: BackupPlugin,
private val kv: KVBackup,
private val full: FullBackup,
private val apkBackup: ApkBackup,
private val clock: Clock,
private val packageService: PackageService,
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager) {
private val context: Context,
private val plugin: BackupPlugin,
private val kv: KVBackup,
private val full: FullBackup,
private val apkBackup: ApkBackup,
private val clock: Clock,
private val packageService: PackageService,
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager
) {
private var calledInitialize = false
private var calledClearBackupData = false
@ -92,7 +93,10 @@ internal class BackupCoordinator(
}
}
fun isAppEligibleForBackup(targetPackage: PackageInfo, @Suppress("UNUSED_PARAMETER") isFullBackup: Boolean): Boolean {
fun isAppEligibleForBackup(
targetPackage: PackageInfo,
@Suppress("UNUSED_PARAMETER") isFullBackup: Boolean
): Boolean {
val packageName = targetPackage.packageName
// Check that the app is not blacklisted by the user
val enabled = settingsManager.isBackupEnabled(packageName)
@ -142,7 +146,11 @@ internal class BackupCoordinator(
Log.i(TAG, "Request incremental backup time. Returned $this")
}
suspend fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
suspend fun performIncrementalBackup(
packageInfo: PackageInfo,
data: ParcelFileDescriptor,
flags: Int
): Int {
cancelReason = UNKNOWN_ERROR
val packageName = packageInfo.packageName
if (packageName == MAGIC_PACKAGE_MANAGER) {
@ -185,7 +193,11 @@ internal class BackupCoordinator(
return result
}
suspend fun performFullBackup(targetPackage: PackageInfo, fileDescriptor: ParcelFileDescriptor, flags: Int): Int {
suspend fun performFullBackup(
targetPackage: PackageInfo,
fileDescriptor: ParcelFileDescriptor,
flags: Int
): Int {
cancelReason = UNKNOWN_ERROR
return full.performFullBackup(targetPackage, fileDescriptor, flags)
}
@ -207,7 +219,7 @@ internal class BackupCoordinator(
*/
suspend fun cancelFullBackup() {
val packageInfo = full.getCurrentPackage()
?: throw AssertionError("Cancelling full backup, but no current package")
?: 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()
@ -224,7 +236,7 @@ internal class BackupCoordinator(
*
* @return the same error codes as [performFullBackup].
*/
fun clearBackupData(packageInfo: PackageInfo): Int {
suspend fun clearBackupData(packageInfo: PackageInfo): Int {
val packageName = packageInfo.packageName
Log.i(TAG, "Clear Backup Data of $packageName.")
try {
@ -254,12 +266,12 @@ internal class BackupCoordinator(
suspend fun finishBackup(): Int = when {
kv.hasState() -> {
check(!full.hasState()) { "K/V backup has state, but full backup has dangling state as well" }
onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state
onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state
kv.finishBackup()
}
full.hasState() -> {
check(!kv.hasState()) { "Full backup has state, but K/V backup has dangling state as well" }
onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state
onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state
full.finishBackup()
}
calledInitialize || calledClearBackupData -> {
@ -281,7 +293,10 @@ internal class BackupCoordinator(
}
}
private suspend fun backUpApk(packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR) {
private suspend fun backUpApk(
packageInfo: PackageInfo,
packageState: PackageState = UNKNOWN_ERROR
) {
val packageName = packageInfo.packageName
try {
apkBackup.backupApkIfNecessary(packageInfo, packageState) {

View file

@ -18,10 +18,11 @@ import java.io.InputStream
import java.io.OutputStream
private class FullBackupState(
internal val packageInfo: PackageInfo,
internal val inputFileDescriptor: ParcelFileDescriptor,
internal val inputStream: InputStream,
internal var outputStreamInit: (suspend () -> OutputStream)?) {
internal val packageInfo: PackageInfo,
internal val inputFileDescriptor: ParcelFileDescriptor,
internal val inputStream: InputStream,
internal var outputStreamInit: (suspend () -> OutputStream)?
) {
internal var outputStream: OutputStream? = null
internal val packageName: String = packageInfo.packageName
internal var size: Long = 0
@ -33,10 +34,11 @@ private val TAG = FullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup(
private val plugin: FullBackupPlugin,
private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter,
private val crypto: Crypto) {
private val plugin: FullBackupPlugin,
private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter,
private val crypto: Crypto
) {
private var state: FullBackupState? = null
@ -90,7 +92,11 @@ internal class FullBackup(
* [TRANSPORT_OK] to indicate that the OS may proceed with delivering backup data;
* [TRANSPORT_ERROR] to indicate an error that precludes performing a backup at this time.
*/
suspend fun performFullBackup(targetPackage: PackageInfo, socket: ParcelFileDescriptor, @Suppress("UNUSED_PARAMETER") flags: Int = 0): Int {
suspend fun performFullBackup(
targetPackage: PackageInfo,
socket: ParcelFileDescriptor,
@Suppress("UNUSED_PARAMETER") flags: Int = 0
): Int {
if (state != null) throw AssertionError()
Log.i(TAG, "Perform full backup for ${targetPackage.packageName}.")
@ -102,7 +108,9 @@ internal class FullBackup(
val outputStream = try {
plugin.getOutputStream(targetPackage)
} catch (e: IOException) {
Log.e(TAG, "Error getting OutputStream for full backup of ${targetPackage.packageName}", e)
"Error getting OutputStream for full backup of ${targetPackage.packageName}".let {
Log.e(TAG, it, e)
}
throw(e)
}
// store version header
@ -116,31 +124,36 @@ internal class FullBackup(
throw(e)
}
outputStream
} // this lambda is only called before we actually write backup data the first time
} // this lambda is only called before we actually write backup data the first time
return TRANSPORT_OK
}
suspend fun sendBackupData(numBytes: Int): Int {
val state = this.state
?: throw AssertionError("Attempted sendBackupData before performFullBackup")
?: throw AssertionError("Attempted sendBackupData before performFullBackup")
// check if size fits quota
state.size += numBytes
val quota = plugin.getQuota()
if (state.size > quota) {
Log.w(TAG, "Full backup of additional $numBytes exceeds quota of $quota with ${state.size}.")
Log.w(
TAG,
"Full backup of additional $numBytes exceeds quota of $quota with ${state.size}."
)
return TRANSPORT_QUOTA_EXCEEDED
}
return try {
// get output stream or initialize it, if it does not yet exist
check((state.outputStream != null) xor (state.outputStreamInit != null)) { "No OutputStream xor no StreamGetter" }
check((state.outputStream != null) xor (state.outputStreamInit != null)) {
"No OutputStream xor no StreamGetter"
}
val outputStream = state.outputStream ?: suspend {
val stream = state.outputStreamInit!!() // not-null due to check above
state.outputStream = stream
stream
}()
state.outputStreamInit = null // the stream init lambda is not needed beyond that point
state.outputStreamInit = null // the stream init lambda is not needed beyond that point
// read backup data, encrypt it and write it to output stream
val payload = IOUtils.readFully(state.inputStream, numBytes)
@ -153,11 +166,11 @@ internal class FullBackup(
}
@Throws(IOException::class)
fun clearBackupData(packageInfo: PackageInfo) {
suspend fun clearBackupData(packageInfo: PackageInfo) {
plugin.removeDataOfPackage(packageInfo)
}
fun cancelFullBackup() {
suspend fun cancelFullBackup() {
Log.i(TAG, "Cancel full backup")
val state = this.state ?: throw AssertionError("No state when canceling")
try {

View file

@ -16,6 +16,6 @@ interface FullBackupPlugin {
* Remove all data associated with the given package.
*/
@Throws(IOException::class)
fun removeDataOfPackage(packageInfo: PackageInfo)
suspend fun removeDataOfPackage(packageInfo: PackageInfo)
}

View file

@ -23,10 +23,11 @@ private val TAG = KVBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackup(
private val plugin: KVBackupPlugin,
private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter,
private val crypto: Crypto) {
private val plugin: KVBackupPlugin,
private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter,
private val crypto: Crypto
) {
private var state: KVBackupState? = null
@ -36,7 +37,11 @@ internal class KVBackup(
fun getQuota(): Long = plugin.getQuota()
suspend fun performBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
suspend fun performBackup(
packageInfo: PackageInfo,
data: ParcelFileDescriptor,
flags: Int
): Int {
val isIncremental = flags and FLAG_INCREMENTAL != 0
val isNonIncremental = flags and FLAG_NON_INCREMENTAL != 0
val packageName = packageInfo.packageName
@ -65,7 +70,10 @@ internal class KVBackup(
return backupError(TRANSPORT_ERROR)
}
if (isIncremental && !hasDataForPackage) {
Log.w(TAG, "Requested incremental, but transport currently stores no data $packageName, requesting non-incremental retry.")
Log.w(
TAG, "Requested incremental, but transport currently stores no data" +
" for $packageName, requesting non-incremental retry."
)
return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)
}
@ -164,7 +172,7 @@ internal class KVBackup(
}
@Throws(IOException::class)
fun clearBackupData(packageInfo: PackageInfo) {
suspend fun clearBackupData(packageInfo: PackageInfo) {
plugin.removeDataOfPackage(packageInfo)
}
@ -179,18 +187,20 @@ internal class KVBackup(
* because [finishBackup] is not called when we don't return [TRANSPORT_OK].
*/
private fun backupError(result: Int): Int {
Log.i(TAG, "Resetting state because of K/V Backup error of ${state!!.packageInfo.packageName}")
"Resetting state because of K/V Backup error of ${state!!.packageInfo.packageName}".let {
Log.i(TAG, it)
}
state = null
return result
}
private class KVOperation(
internal val key: String,
internal val base64Key: String,
/**
* value is null when this is a deletion operation
*/
internal val value: ByteArray?
internal val key: String,
internal val base64Key: String,
/**
* value is null when this is a deletion operation
*/
internal val value: ByteArray?
)
private sealed class Result<out T> {

View file

@ -16,7 +16,7 @@ interface KVBackupPlugin {
* Return true if there are records stored for the given package.
*/
@Throws(IOException::class)
fun hasDataForPackage(packageInfo: PackageInfo): Boolean
suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean
/**
* This marks the beginning of a backup operation.
@ -38,12 +38,12 @@ interface KVBackupPlugin {
* Delete the record for the given package identified by the given key.
*/
@Throws(IOException::class)
fun deleteRecord(packageInfo: PackageInfo, key: String)
suspend fun deleteRecord(packageInfo: PackageInfo, key: String)
/**
* Remove all data associated with the given package.
*/
@Throws(IOException::class)
fun removeDataOfPackage(packageInfo: PackageInfo)
suspend fun removeDataOfPackage(packageInfo: PackageInfo)
}

View file

@ -15,24 +15,27 @@ import com.stevesoltys.seedvault.header.HeaderReader
import com.stevesoltys.seedvault.header.UnsupportedVersionException
import libcore.io.IoUtils.closeQuietly
import java.io.IOException
import java.util.*
import java.util.ArrayList
import javax.crypto.AEADBadTagException
private class KVRestoreState(
internal val token: Long,
internal val packageInfo: PackageInfo,
/**
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
*/
internal val pmPackageInfo: PackageInfo?)
internal val token: Long,
internal val packageInfo: PackageInfo,
/**
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
*/
internal val pmPackageInfo: PackageInfo?
)
private val TAG = KVRestore::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVRestore(
private val plugin: KVRestorePlugin,
private val outputFactory: OutputFactory,
private val headerReader: HeaderReader,
private val crypto: Crypto) {
private val plugin: KVRestorePlugin,
private val outputFactory: OutputFactory,
private val headerReader: HeaderReader,
private val crypto: Crypto
) {
private var state: KVRestoreState? = null
@ -63,7 +66,7 @@ internal class KVRestore(
* @return One of [TRANSPORT_OK]
* or [TRANSPORT_ERROR] (an error occurred, the restore should be aborted and rescheduled).
*/
fun getRestoreData(data: ParcelFileDescriptor): Int {
suspend fun getRestoreData(data: ParcelFileDescriptor): Int {
val state = this.state ?: throw IllegalStateException("no state")
// The restore set is the concatenation of the individual record blobs,
@ -122,11 +125,12 @@ internal class KVRestore(
for (recordKey in records) contents.add(DecodedKey(recordKey))
// remove keys that are not needed for single package @pm@ restore
val pmPackageName = state?.pmPackageInfo?.packageName
val sortedKeys = if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) {
val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName)
Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName")
contents.filterTo(ArrayList()) { it.key in keys }
} else contents
val sortedKeys =
if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) {
val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName)
Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName")
contents.filterTo(ArrayList()) { it.key in keys }
} else contents
sortedKeys.sort()
return sortedKeys
}
@ -135,8 +139,13 @@ internal class KVRestore(
* Read the encrypted value for the given key and write it to the given [BackupDataOutput].
*/
@Throws(IOException::class, UnsupportedVersionException::class, SecurityException::class)
private fun readAndWriteValue(state: KVRestoreState, dKey: DecodedKey, out: BackupDataOutput) {
val inputStream = plugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
private suspend fun readAndWriteValue(
state: KVRestoreState,
dKey: DecodedKey,
out: BackupDataOutput
) {
val inputStream =
plugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
try {
val version = headerReader.readVersion(inputStream)
crypto.decryptHeader(inputStream, version, state.packageInfo.packageName, dKey.key)

View file

@ -29,6 +29,6 @@ interface KVRestorePlugin {
* Note: Implementations might expect that you call [hasDataForPackage] before.
*/
@Throws(IOException::class)
fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream
suspend fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream
}

View file

@ -26,12 +26,13 @@ import libcore.io.IoUtils.closeQuietly
import java.io.IOException
private class RestoreCoordinatorState(
internal val token: Long,
internal val packages: Iterator<PackageInfo>,
/**
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/
internal val pmPackageInfo: PackageInfo?) {
internal val token: Long,
internal val packages: Iterator<PackageInfo>,
/**
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/
internal val pmPackageInfo: PackageInfo?
) {
internal var currentPackage: String? = null
}
@ -39,14 +40,15 @@ private val TAG = RestoreCoordinator::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreCoordinator(
private val context: Context,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager,
private val plugin: RestorePlugin,
private val kv: KVRestore,
private val full: FullRestore,
private val metadataReader: MetadataReader) {
private val context: Context,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager,
private val plugin: RestorePlugin,
private val kv: KVRestore,
private val full: FullRestore,
private val metadataReader: MetadataReader
) {
private var state: RestoreCoordinatorState? = null
private var backupMetadata: LongSparseArray<BackupMetadata>? = null
@ -68,7 +70,10 @@ internal class RestoreCoordinator(
"No error when getting encrypted metadata, but stream is still missing."
}
try {
val metadata = metadataReader.readMetadata(encryptedMetadata.inputStream, encryptedMetadata.token)
val metadata = metadataReader.readMetadata(
encryptedMetadata.inputStream,
encryptedMetadata.token
)
metadataMap.put(encryptedMetadata.token, metadata)
val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token)
restoreSets.add(set)
@ -102,7 +107,7 @@ internal class RestoreCoordinator(
*/
fun getCurrentRestoreSet(): Long {
return metadataManager.getBackupToken()
.apply { Log.i(TAG, "Got current restore set token: $this") }
.apply { Log.i(TAG, "Got current restore set token: $this") }
}
/**
@ -122,22 +127,26 @@ internal class RestoreCoordinator(
Log.i(TAG, "Start restore with ${packages.map { info -> info.packageName }}")
// If there's only one package to restore (Auto Restore feature), add it to the state
val pmPackageInfo = if (packages.size == 2 && packages[0].packageName == MAGIC_PACKAGE_MANAGER) {
val pmPackageName = packages[1].packageName
Log.d(TAG, "Optimize for single package restore of $pmPackageName")
// check if the backup is on removable storage that is not plugged in
if (isStorageRemovableAndNotAvailable()) {
// check if we even have a backup of that app
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
// remind user to plug in storage device
val storageName = settingsManager.getStorage()?.name
val pmPackageInfo =
if (packages.size == 2 && packages[0].packageName == MAGIC_PACKAGE_MANAGER) {
val pmPackageName = packages[1].packageName
Log.d(TAG, "Optimize for single package restore of $pmPackageName")
// check if the backup is on removable storage that is not plugged in
if (isStorageRemovableAndNotAvailable()) {
// check if we even have a backup of that app
if (metadataManager.getPackageMetadata(pmPackageName) != null) {
// remind user to plug in storage device
val storageName = settingsManager.getStorage()?.name
?: context.getString(R.string.settings_backup_location_none)
notificationManager.onRemovableStorageNotAvailableForRestore(pmPackageName, storageName)
notificationManager.onRemovableStorageNotAvailableForRestore(
pmPackageName,
storageName
)
}
return TRANSPORT_ERROR
}
return TRANSPORT_ERROR
}
packages[1]
} else null
packages[1]
} else null
state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo)
failedPackages.clear()
@ -214,7 +223,7 @@ internal class RestoreCoordinator(
* @param data An open, writable file into which the key/value backup data should be stored.
* @return the same error codes as [startRestore].
*/
fun getRestoreData(data: ParcelFileDescriptor): Int {
suspend fun getRestoreData(data: ParcelFileDescriptor): Int {
return kv.getRestoreData(data).apply {
if (this != TRANSPORT_OK) {
// add current package to failed ones

View file

@ -68,16 +68,37 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val fullBackupPlugin = mockk<FullBackupPlugin>()
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
private val apkBackup = mockk<ApkBackup>()
private val packageService:PackageService = mockk()
private val packageService: PackageService = mockk()
private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, clock, packageService, metadataManager, settingsManager, notificationManager)
private val backup = BackupCoordinator(
context,
backupPlugin,
kvBackup,
fullBackup,
apkBackup,
clock,
packageService,
metadataManager,
settingsManager,
notificationManager
)
private val restorePlugin = mockk<RestorePlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>()
private val kvRestore = KVRestore(kvRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val fullRestorePlugin = mockk<FullRestorePlugin>()
private val fullRestore = FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, restorePlugin, kvRestore, fullRestore, metadataReader)
private val fullRestore =
FullRestore(fullRestorePlugin, outputFactory, headerReader, cryptoImpl)
private val restore = RestoreCoordinator(
context,
settingsManager,
metadataManager,
notificationManager,
restorePlugin,
kvRestore,
fullRestore,
metadataReader
)
private val backupDataInput = mockk<BackupDataInput>()
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
@ -104,7 +125,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val bOutputStream2 = ByteArrayOutputStream()
// read one key/value record and write it to output stream
every { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
every { backupDataInput.readNextHeader() } returns true andThen true andThen false
@ -114,15 +135,37 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData.copyInto(value.captured) // write the app data into the passed ByteArray
appData.size
}
coEvery { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream
coEvery {
kvBackupPlugin.getOutputStreamForRecord(
packageInfo,
key64
)
} returns bOutputStream
every { backupDataInput.readEntityData(capture(value2), 0, appData2.size) } answers {
appData2.copyInto(value2.captured) // write the app data into the passed ByteArray
appData2.size
}
coEvery { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
coEvery {
kvBackupPlugin.getOutputStreamForRecord(
packageInfo,
key264
)
} returns bOutputStream2
coEvery {
apkBackup.backupApkIfNecessary(
packageInfo,
UNKNOWN_ERROR,
any()
)
} returns packageMetadata
coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs
every {
metadataManager.onApkBackedUp(
packageInfo,
packageMetadata,
metadataOutputStream
)
} just Runs
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
// start and finish K/V backup
@ -145,10 +188,22 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val rInputStream2 = ByteArrayInputStream(bOutputStream2.toByteArray())
every { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64, key264)
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput
every { kvRestorePlugin.getInputStreamForRecord(token, packageInfo, key64) } returns rInputStream
coEvery {
kvRestorePlugin.getInputStreamForRecord(
token,
packageInfo,
key64
)
} returns rInputStream
every { backupDataOutput.writeEntityHeader(key, appData.size) } returns 1137
every { backupDataOutput.writeEntityData(appData, appData.size) } returns appData.size
every { kvRestorePlugin.getInputStreamForRecord(token, packageInfo, key264) } returns rInputStream2
coEvery {
kvRestorePlugin.getInputStreamForRecord(
token,
packageInfo,
key264
)
} returns rInputStream2
every { backupDataOutput.writeEntityHeader(key2, appData2.size) } returns 1137
every { backupDataOutput.writeEntityData(appData2, appData2.size) } returns appData2.size
@ -163,7 +218,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val bOutputStream = ByteArrayOutputStream()
// read one key/value record and write it to output stream
every { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
every { backupDataInput.readNextHeader() } returns true andThen false
@ -173,7 +228,12 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData.copyInto(value.captured) // write the app data into the passed ByteArray
appData.size
}
coEvery { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream
coEvery {
kvBackupPlugin.getOutputStreamForRecord(
packageInfo,
key64
)
} returns bOutputStream
coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null
coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
@ -197,7 +257,13 @@ internal class CoordinatorIntegrationTest : TransportTest() {
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
every { kvRestorePlugin.listRecords(token, packageInfo) } returns listOf(key64)
every { outputFactory.getBackupDataOutput(fileDescriptor) } returns backupDataOutput
every { kvRestorePlugin.getInputStreamForRecord(token, packageInfo, key64) } returns rInputStream
coEvery {
kvRestorePlugin.getInputStreamForRecord(
token,
packageInfo,
key64
)
} returns rInputStream
every { backupDataOutput.writeEntityHeader(key, appData.size) } returns 1137
every { backupDataOutput.writeEntityData(appData, appData.size) } returns appData.size
@ -212,9 +278,21 @@ internal class CoordinatorIntegrationTest : TransportTest() {
coEvery { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata
coEvery {
apkBackup.backupApkIfNecessary(
packageInfo,
UNKNOWN_ERROR,
any()
)
} returns packageMetadata
coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onApkBackedUp(packageInfo, packageMetadata, metadataOutputStream) } just Runs
every {
metadataManager.onApkBackedUp(
packageInfo,
packageMetadata,
metadataOutputStream
)
} just Runs
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs
// perform backup to output stream
@ -237,7 +315,12 @@ internal class CoordinatorIntegrationTest : TransportTest() {
// reverse the backup streams into restore input
val rInputStream = ByteArrayInputStream(bOutputStream.toByteArray())
val rOutputStream = ByteArrayOutputStream()
coEvery { fullRestorePlugin.getInputStreamForPackage(token, packageInfo) } returns rInputStream
coEvery {
fullRestorePlugin.getInputStreamForPackage(
token,
packageInfo
)
} returns rInputStream
every { outputFactory.getOutputStream(fileDescriptor) } returns rOutputStream
// restore data

View file

@ -44,7 +44,18 @@ internal class BackupCoordinatorTest : BackupTest() {
private val packageService: PackageService = mockk()
private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, packageService, metadataManager, settingsManager, notificationManager)
private val backup = BackupCoordinator(
context,
plugin,
kv,
full,
apkBackup,
clock,
packageService,
metadataManager,
settingsManager,
notificationManager
)
private val metadataOutputStream = mockk<OutputStream>()
private val fileDescriptor: ParcelFileDescriptor = mockk()
@ -93,26 +104,27 @@ internal class BackupCoordinatorTest : BackupTest() {
}
@Test
fun `no error notification when device initialization fails on unplugged USB storage`() = runBlocking {
val storage = mockk<Storage>()
val documentFile = mockk<DocumentFile>()
fun `no error notification when device initialization fails on unplugged USB storage`() =
runBlocking {
val storage = mockk<Storage>()
val documentFile = mockk<DocumentFile>()
every { clock.time() } returns token
coEvery { plugin.initializeDevice(token) } throws IOException()
every { settingsManager.getStorage() } returns storage
every { storage.isUsb } returns true
every { storage.getDocumentFile(context) } returns documentFile
every { documentFile.isDirectory } returns false
every { clock.time() } returns token
coEvery { plugin.initializeDevice(token) } throws IOException()
every { settingsManager.getStorage() } returns storage
every { storage.isUsb } returns true
every { storage.getDocumentFile(context) } returns documentFile
every { documentFile.isDirectory } returns false
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
assertEquals(TRANSPORT_ERROR, backup.initializeDevice())
// finish will only be called when TRANSPORT_OK is returned, so it should throw
every { kv.hasState() } returns false
every { full.hasState() } returns false
coAssertThrows(IllegalStateException::class.java) {
backup.finishBackup()
// finish will only be called when TRANSPORT_OK is returned, so it should throw
every { kv.hasState() } returns false
every { full.hasState() } returns false
coAssertThrows(IllegalStateException::class.java) {
backup.finishBackup()
}
}
}
@Test
fun `getBackupQuota() delegates to right plugin`() = runBlocking {
@ -143,24 +155,24 @@ internal class BackupCoordinatorTest : BackupTest() {
}
@Test
fun `clearing KV backup data throws`() {
every { kv.clearBackupData(packageInfo) } throws IOException()
fun `clearing KV backup data throws`() = runBlocking {
coEvery { kv.clearBackupData(packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.clearBackupData(packageInfo))
}
@Test
fun `clearing full backup data throws`() {
every { kv.clearBackupData(packageInfo) } just Runs
every { full.clearBackupData(packageInfo) } throws IOException()
fun `clearing full backup data throws`() = runBlocking {
coEvery { kv.clearBackupData(packageInfo) } just Runs
coEvery { full.clearBackupData(packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.clearBackupData(packageInfo))
}
@Test
fun `clearing backup data succeeds`() = runBlocking {
every { kv.clearBackupData(packageInfo) } just Runs
every { full.clearBackupData(packageInfo) } just Runs
coEvery { kv.clearBackupData(packageInfo) } just Runs
coEvery { full.clearBackupData(packageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.clearBackupData(packageInfo))
@ -213,16 +225,28 @@ internal class BackupCoordinatorTest : BackupTest() {
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, QUOTA_EXCEEDED, metadataOutputStream) } just Runs
every { full.cancelFullBackup() } just Runs
every {
metadataManager.onPackageBackupError(
packageInfo,
QUOTA_EXCEEDED,
metadataOutputStream
)
} just Runs
coEvery { 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))
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())
@ -238,14 +262,24 @@ internal class BackupCoordinatorTest : BackupTest() {
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, NO_DATA, metadataOutputStream) } just Runs
every { full.cancelFullBackup() } just Runs
every {
metadataManager.onPackageBackupError(
packageInfo,
NO_DATA,
metadataOutputStream
)
} just Runs
coEvery { 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_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())
@ -259,24 +293,44 @@ internal class BackupCoordinatorTest : BackupTest() {
fun `not allowed apps get their APKs backed up during @pm@ backup`() = runBlocking {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
val notAllowedPackages = listOf(
PackageInfo().apply { packageName = "org.example.1" },
PackageInfo().apply { packageName = "org.example.2" }
PackageInfo().apply { packageName = "org.example.1" },
PackageInfo().apply { packageName = "org.example.2" }
)
val packageMetadata: PackageMetadata = mockk()
every { settingsManager.getStorage() } returns storage // to check for removable storage
every { settingsManager.getStorage() } returns storage // to check for removable storage
every { packageService.notAllowedPackages } returns notAllowedPackages
// no backup needed
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) } returns null
coEvery {
apkBackup.backupApkIfNecessary(
notAllowedPackages[0],
NOT_ALLOWED,
any()
)
} returns null
// was backed up, get new packageMetadata
coEvery { apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) } returns packageMetadata
coEvery {
apkBackup.backupApkIfNecessary(
notAllowedPackages[1],
NOT_ALLOWED,
any()
)
} returns packageMetadata
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onApkBackedUp(notAllowedPackages[1], packageMetadata, metadataOutputStream) } just Runs
every {
metadataManager.onApkBackedUp(
notAllowedPackages[1],
packageMetadata,
metadataOutputStream
)
} just Runs
// do actual @pm@ backup
coEvery { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
assertEquals(TRANSPORT_OK,
backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
assertEquals(
TRANSPORT_OK,
backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)
)
coVerify {
apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
@ -285,9 +339,21 @@ internal class BackupCoordinatorTest : BackupTest() {
}
private fun expectApkBackupAndMetadataWrite() {
coEvery { apkBackup.backupApkIfNecessary(any(), UNKNOWN_ERROR, any()) } returns packageMetadata
coEvery {
apkBackup.backupApkIfNecessary(
any(),
UNKNOWN_ERROR,
any()
)
} returns packageMetadata
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onApkBackedUp(any(), packageMetadata, metadataOutputStream) } just Runs
every {
metadataManager.onApkBackedUp(
any(),
packageMetadata,
metadataOutputStream
)
} just Runs
}
}

View file

@ -199,8 +199,8 @@ internal class FullBackupTest : BackupTest() {
}
@Test
fun `clearBackupData delegates to plugin`() {
every { plugin.removeDataOfPackage(packageInfo) } just Runs
fun `clearBackupData delegates to plugin`() = runBlocking {
coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs
backup.clearBackupData(packageInfo)
}
@ -210,7 +210,7 @@ internal class FullBackupTest : BackupTest() {
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
expectClearState()
every { plugin.removeDataOfPackage(packageInfo) } just Runs
coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data))
assertTrue(backup.hasState())
@ -223,7 +223,7 @@ internal class FullBackupTest : BackupTest() {
every { inputFactory.getInputStream(data) } returns inputStream
expectInitializeOutputStream()
expectClearState()
every { plugin.removeDataOfPackage(packageInfo) } throws IOException()
coEvery { plugin.removeDataOfPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data))
assertTrue(backup.hasState())

View file

@ -54,7 +54,7 @@ internal class KVBackupTest : BackupTest() {
@Test
fun `incremental backup with no data gets rejected`() = runBlocking {
every { plugin.hasDataForPackage(packageInfo) } returns false
coEvery { plugin.hasDataForPackage(packageInfo) } returns false
assertEquals(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, backup.performBackup(packageInfo, data, FLAG_INCREMENTAL))
assertFalse(backup.hasState())
@ -62,7 +62,7 @@ internal class KVBackupTest : BackupTest() {
@Test
fun `check for existing data throws exception`() = runBlocking {
every { plugin.hasDataForPackage(packageInfo) } throws IOException()
coEvery { plugin.hasDataForPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState())
@ -71,7 +71,7 @@ internal class KVBackupTest : BackupTest() {
@Test
fun `non-incremental backup with data clears old data first`() = runBlocking {
singleRecordBackup(true)
every { plugin.removeDataOfPackage(packageInfo) } just Runs
coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL))
assertTrue(backup.hasState())
@ -82,7 +82,7 @@ internal class KVBackupTest : BackupTest() {
@Test
fun `ignoring exception when clearing data when non-incremental backup has data`() = runBlocking {
singleRecordBackup(true)
every { plugin.removeDataOfPackage(packageInfo) } throws IOException()
coEvery { plugin.removeDataOfPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL))
assertTrue(backup.hasState())
@ -92,7 +92,7 @@ internal class KVBackupTest : BackupTest() {
@Test
fun `ensuring storage throws exception`() = runBlocking {
every { plugin.hasDataForPackage(packageInfo) } returns false
coEvery { plugin.hasDataForPackage(packageInfo) } returns false
coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
@ -194,7 +194,7 @@ internal class KVBackupTest : BackupTest() {
}
private fun initPlugin(hasDataForPackage: Boolean = false) {
every { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage
coEvery { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage
coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } just Runs
}

View file

@ -3,6 +3,7 @@ package com.stevesoltys.seedvault.transport.restore
import android.app.backup.BackupDataOutput
import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK
import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.header.UnsupportedVersionException
@ -16,7 +17,6 @@ import io.mockk.mockk
import io.mockk.verifyAll
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import java.io.IOException
import java.io.InputStream
@ -47,13 +47,13 @@ internal class KVRestoreTest : RestoreTest() {
@Test
fun `getRestoreData() throws without initializing state`() {
assertThrows(IllegalStateException::class.java) {
coAssertThrows(IllegalStateException::class.java) {
restore.getRestoreData(fileDescriptor)
}
}
@Test
fun `listing records throws`() {
fun `listing records throws`() = runBlocking {
restore.initializeState(token, packageInfo)
every { plugin.listRecords(token, packageInfo) } throws IOException()
@ -62,11 +62,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `reading VersionHeader with unsupported version throws`() {
fun `reading VersionHeader with unsupported version throws`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion)
streamsGetClosed()
@ -75,11 +75,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `error reading VersionHeader throws`() {
fun `error reading VersionHeader throws`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } throws IOException()
streamsGetClosed()
@ -88,11 +88,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `decrypting segment throws`() {
fun `decrypting segment throws`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } throws IOException()
@ -103,11 +103,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `decrypting header throws`() {
fun `decrypting header throws`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws IOException()
streamsGetClosed()
@ -117,11 +117,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `decrypting header throws security exception`() {
fun `decrypting header throws security exception`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws SecurityException()
streamsGetClosed()
@ -131,11 +131,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `writing header throws`() {
fun `writing header throws`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data
@ -147,11 +147,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `writing value throws`() {
fun `writing value throws`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data
@ -164,11 +164,11 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `writing value succeeds`() {
fun `writing value succeeds`() = runBlocking {
restore.initializeState(token, packageInfo)
getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data
@ -181,21 +181,21 @@ internal class KVRestoreTest : RestoreTest() {
}
@Test
fun `writing two values succeeds`() {
fun `writing two values succeeds`() = runBlocking {
val data2 = getRandomByteArray()
val inputStream2 = mockk<InputStream>()
restore.initializeState(token, packageInfo)
getRecordsAndOutput(listOf(key64, key264))
// first key/value
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data
every { output.writeEntityHeader(key, data.size) } returns 42
every { output.writeEntityData(data, data.size) } returns data.size
// second key/value
every { plugin.getInputStreamForRecord(token, packageInfo, key264) } returns inputStream2
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key264) } returns inputStream2
every { headerReader.readVersion(inputStream2) } returns VERSION
every { crypto.decryptHeader(inputStream2, VERSION, packageInfo.packageName, key2) } returns versionHeader2
every { crypto.decryptMultipleSegments(inputStream2) } returns data2

View file

@ -44,7 +44,16 @@ internal class RestoreCoordinatorTest : TransportTest() {
private val full = mockk<FullRestore>()
private val metadataReader = mockk<MetadataReader>()
private val restore = RestoreCoordinator(context, settingsManager, metadataManager, notificationManager, plugin, kv, full, metadataReader)
private val restore = RestoreCoordinator(
context,
settingsManager,
metadataManager,
notificationManager,
plugin,
kv,
full,
metadataReader
)
private val token = Random.nextLong()
private val inputStream = mockk<InputStream>()
@ -219,11 +228,11 @@ internal class RestoreCoordinatorTest : TransportTest() {
}
@Test
fun `getRestoreData() delegates to KV`() {
fun `getRestoreData() delegates to KV`() = runBlocking {
val data = mockk<ParcelFileDescriptor>()
val result = Random.nextInt()
every { kv.getRestoreData(data) } returns result
coEvery { kv.getRestoreData(data) } returns result
assertEquals(result, restore.getRestoreData(data))
}