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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,17 +16,18 @@ import java.io.InputStream
private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName private val TAG = DocumentsProviderRestorePlugin::class.java.simpleName
@WorkerThread @WorkerThread
@Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O @Suppress("BlockingMethodInNonBlockingContext") // all methods do I/O
internal class DocumentsProviderRestorePlugin( internal class DocumentsProviderRestorePlugin(
private val context: Context, private val context: Context,
private val storage: DocumentsStorage) : RestorePlugin { private val storage: DocumentsStorage
) : RestorePlugin {
override val kvRestorePlugin: KVRestorePlugin by lazy { override val kvRestorePlugin: KVRestorePlugin by lazy {
DocumentsProviderKVRestorePlugin(storage) DocumentsProviderKVRestorePlugin(context, storage)
} }
override val fullRestorePlugin: FullRestorePlugin by lazy { override val fullRestorePlugin: FullRestorePlugin by lazy {
DocumentsProviderFullRestorePlugin(storage) DocumentsProviderFullRestorePlugin(context, storage)
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -42,7 +43,7 @@ internal class DocumentsProviderRestorePlugin(
val backupSets = getBackups(context, rootDir) val backupSets = getBackups(context, rootDir)
val iterator = backupSets.iterator() val iterator = backupSets.iterator()
return generateSequence { return generateSequence {
if (!iterator.hasNext()) return@generateSequence null // end sequence if (!iterator.hasNext()) return@generateSequence null // end sequence
val backupSet = iterator.next() val backupSet = iterator.next()
try { try {
val stream = storage.getInputStream(backupSet.metadataFile) val stream = storage.getInputStream(backupSet.metadataFile)
@ -64,18 +65,9 @@ internal class DocumentsProviderRestorePlugin(
return backupSets return backupSets
} }
for (set in files) { for (set in files) {
if (!set.isDirectory || set.name == null) { // get current token from set or continue to next file/set
if (set.name != FILE_NO_MEDIA) { val token = set.getTokenOrNull() ?: continue
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
}
// block until children of set are available // block until children of set are available
val metadata = try { val metadata = try {
set.findFileBlocking(context, FILE_BACKUP_METADATA) set.findFileBlocking(context, FILE_BACKUP_METADATA)
@ -92,10 +84,26 @@ internal class DocumentsProviderRestorePlugin(
return backupSets 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) @Throws(IOException::class)
override suspend fun getApkInputStream(token: Long, packageName: String): InputStream { override suspend fun getApkInputStream(token: Long, packageName: String): InputStream {
val setDir = storage.getSetDir(token) ?: throw IOException() 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) 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 private val TAG = DocumentsStorage::class.java.simpleName
internal class DocumentsStorage( internal class DocumentsStorage(
private val context: Context, private val context: Context,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager private val settingsManager: SettingsManager
) { ) {
private val contentResolver = context.contentResolver private val contentResolver = context.contentResolver
@ -58,7 +58,7 @@ internal class DocumentsStorage(
get() = runBlocking { get() = runBlocking {
if (field == null) { if (field == null) {
val parent = storage?.getDocumentFile(context) val parent = storage?.getDocumentFile(context)
?: return@runBlocking null ?: return@runBlocking null
field = try { field = try {
parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply { parent.createOrGetDirectory(context, DIRECTORY_ROOT).apply {
// create .nomedia file to prevent Android's MediaScanner // 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)". * If we were trying to create it right away, some providers create "filename (1)".
*/ */
@Throws(IOException::class) @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 { return findFileBlocking(context, name) ?: createFile(mimeType, name)?.apply {
check(this.name == name) { "File named ${this.name}, but should be $name" } check(this.name == name) { "File named ${this.name}, but should be $name" }
} ?: throw IOException() } ?: throw IOException()
@ -276,25 +280,26 @@ suspend fun DocumentFile.findFileBlocking(context: Context, displayName: String)
*/ */
@VisibleForTesting @VisibleForTesting
@Throws(IOException::class, TimeoutCancellationException::class) @Throws(IOException::class, TimeoutCancellationException::class)
internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) = withTimeout(timeout) { internal suspend fun getLoadedCursor(timeout: Long = 15_000, query: () -> Cursor?) =
suspendCancellableCoroutine<Cursor> { cont -> withTimeout(timeout) {
val cursor = query() ?: throw IOException() suspendCancellableCoroutine<Cursor> { cont ->
cont.invokeOnCancellation { closeQuietly(cursor) } val cursor = query() ?: throw IOException()
val loading = cursor.extras.getBoolean(EXTRA_LOADING, false) cont.invokeOnCancellation { closeQuietly(cursor) }
if (loading) { val loading = cursor.extras.getBoolean(EXTRA_LOADING, false)
Log.d(TAG, "Wait for children to get loaded...") if (loading) {
cursor.registerContentObserver(object : ContentObserver(null) { Log.d(TAG, "Wait for children to get loaded...")
override fun onChange(selfChange: Boolean, uri: Uri?) { cursor.registerContentObserver(object : ContentObserver(null) {
Log.d(TAG, "Children loaded. Continue...") override fun onChange(selfChange: Boolean, uri: Uri?) {
closeQuietly(cursor) Log.d(TAG, "Children loaded. Continue...")
val newCursor = query() closeQuietly(cursor)
if (newCursor == null) cont.cancel(IOException("query returned no results")) val newCursor = query()
else cont.resume(newCursor) if (newCursor == null) cont.cancel(IOException("query returned no results"))
} else cont.resume(newCursor)
}) }
} else { })
// not loading, return cursor right away } else {
cont.resume(cursor) // 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 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 private val TAG = ConfigurableBackupTransport::class.java.simpleName
/** /**
* @author Steve Soltys * @author Steve Soltys
* @author Torsten Grote * @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 backupCoordinator by inject<BackupCoordinator>()
private val restoreCoordinator by inject<RestoreCoordinator>() private val restoreCoordinator by inject<RestoreCoordinator>()
@ -62,7 +64,10 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
backupCoordinator.initializeDevice() backupCoordinator.initializeDevice()
} }
override fun isAppEligibleForBackup(targetPackage: PackageInfo, isFullBackup: Boolean): Boolean { override fun isAppEligibleForBackup(
targetPackage: PackageInfo,
isFullBackup: Boolean
): Boolean {
return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup) return backupCoordinator.isAppEligibleForBackup(targetPackage, isFullBackup)
} }
@ -70,8 +75,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
backupCoordinator.getBackupQuota(packageName, isFullBackup) backupCoordinator.getBackupQuota(packageName, isFullBackup)
} }
override fun clearBackupData(packageInfo: PackageInfo): Int { override fun clearBackupData(packageInfo: PackageInfo): Int = runBlocking {
return backupCoordinator.clearBackupData(packageInfo) backupCoordinator.clearBackupData(packageInfo)
} }
override fun finishBackup(): Int = runBlocking { override fun finishBackup(): Int = runBlocking {
@ -86,11 +91,18 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
return backupCoordinator.requestBackupTime() 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) 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.") Log.w(TAG, "Warning: Legacy performBackup() method called.")
return performBackup(targetPackage, fileDescriptor, 0) return performBackup(targetPackage, fileDescriptor, 0)
} }
@ -107,11 +119,18 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
return backupCoordinator.checkFullBackupSize(size) 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) 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.") Log.w(TAG, "Warning: Legacy performFullBackup() method called.")
backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0) backupCoordinator.performFullBackup(targetPackage, fileDescriptor, 0)
} }
@ -148,8 +167,8 @@ class ConfigurableBackupTransport internal constructor(private val context: Cont
restoreCoordinator.nextRestorePackage() restoreCoordinator.nextRestorePackage()
} }
override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int { override fun getRestoreData(outputFileDescriptor: ParcelFileDescriptor): Int = runBlocking {
return restoreCoordinator.getRestoreData(outputFileDescriptor) restoreCoordinator.getRestoreData(outputFileDescriptor)
} }
override fun abortFullRestore(): Int { 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 @WorkerThread // entire class should always be accessed from a worker thread, so blocking is ok
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class BackupCoordinator( internal class BackupCoordinator(
private val context: Context, private val context: Context,
private val plugin: BackupPlugin, private val plugin: BackupPlugin,
private val kv: KVBackup, private val kv: KVBackup,
private val full: FullBackup, private val full: FullBackup,
private val apkBackup: ApkBackup, private val apkBackup: ApkBackup,
private val clock: Clock, private val clock: Clock,
private val packageService: PackageService, private val packageService: PackageService,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager) { private val nm: BackupNotificationManager
) {
private var calledInitialize = false private var calledInitialize = false
private var calledClearBackupData = 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 val packageName = targetPackage.packageName
// Check that the app is not blacklisted by the user // Check that the app is not blacklisted by the user
val enabled = settingsManager.isBackupEnabled(packageName) val enabled = settingsManager.isBackupEnabled(packageName)
@ -142,7 +146,11 @@ internal class BackupCoordinator(
Log.i(TAG, "Request incremental backup time. Returned $this") 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 cancelReason = UNKNOWN_ERROR
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
if (packageName == MAGIC_PACKAGE_MANAGER) { if (packageName == MAGIC_PACKAGE_MANAGER) {
@ -185,7 +193,11 @@ internal class BackupCoordinator(
return result 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 cancelReason = UNKNOWN_ERROR
return full.performFullBackup(targetPackage, fileDescriptor, flags) return full.performFullBackup(targetPackage, fileDescriptor, flags)
} }
@ -207,7 +219,7 @@ internal class BackupCoordinator(
*/ */
suspend fun cancelFullBackup() { suspend fun cancelFullBackup() {
val packageInfo = full.getCurrentPackage() 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") Log.i(TAG, "Cancel full backup of ${packageInfo.packageName} because of $cancelReason")
onPackageBackupError(packageInfo) onPackageBackupError(packageInfo)
full.cancelFullBackup() full.cancelFullBackup()
@ -224,7 +236,7 @@ internal class BackupCoordinator(
* *
* @return the same error codes as [performFullBackup]. * @return the same error codes as [performFullBackup].
*/ */
fun clearBackupData(packageInfo: PackageInfo): Int { suspend fun clearBackupData(packageInfo: PackageInfo): Int {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
Log.i(TAG, "Clear Backup Data of $packageName.") Log.i(TAG, "Clear Backup Data of $packageName.")
try { try {
@ -254,12 +266,12 @@ internal class BackupCoordinator(
suspend fun finishBackup(): Int = when { suspend 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" }
onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state
kv.finishBackup() kv.finishBackup()
} }
full.hasState() -> { full.hasState() -> {
check(!kv.hasState()) { "Full backup has state, but K/V backup has dangling state as well" } 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() full.finishBackup()
} }
calledInitialize || calledClearBackupData -> { 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 val packageName = packageInfo.packageName
try { try {
apkBackup.backupApkIfNecessary(packageInfo, packageState) { apkBackup.backupApkIfNecessary(packageInfo, packageState) {

View file

@ -18,10 +18,11 @@ import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
private class FullBackupState( private class FullBackupState(
internal val packageInfo: PackageInfo, internal val packageInfo: PackageInfo,
internal val inputFileDescriptor: ParcelFileDescriptor, internal val inputFileDescriptor: ParcelFileDescriptor,
internal val inputStream: InputStream, internal val inputStream: InputStream,
internal var outputStreamInit: (suspend () -> OutputStream)?) { internal var outputStreamInit: (suspend () -> OutputStream)?
) {
internal var outputStream: OutputStream? = null internal var outputStream: OutputStream? = null
internal val packageName: String = packageInfo.packageName internal val packageName: String = packageInfo.packageName
internal var size: Long = 0 internal var size: Long = 0
@ -33,10 +34,11 @@ private val TAG = FullBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class FullBackup( internal class FullBackup(
private val plugin: FullBackupPlugin, private val plugin: FullBackupPlugin,
private val inputFactory: InputFactory, private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter, private val headerWriter: HeaderWriter,
private val crypto: Crypto) { private val crypto: Crypto
) {
private var state: FullBackupState? = null 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_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. * [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() if (state != null) throw AssertionError()
Log.i(TAG, "Perform full backup for ${targetPackage.packageName}.") Log.i(TAG, "Perform full backup for ${targetPackage.packageName}.")
@ -102,7 +108,9 @@ internal class FullBackup(
val outputStream = try { val outputStream = try {
plugin.getOutputStream(targetPackage) plugin.getOutputStream(targetPackage)
} catch (e: IOException) { } 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) throw(e)
} }
// store version header // store version header
@ -116,31 +124,36 @@ internal class FullBackup(
throw(e) throw(e)
} }
outputStream 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 return TRANSPORT_OK
} }
suspend fun sendBackupData(numBytes: Int): Int { suspend fun sendBackupData(numBytes: Int): Int {
val state = this.state val state = this.state
?: throw AssertionError("Attempted sendBackupData before performFullBackup") ?: throw AssertionError("Attempted sendBackupData before performFullBackup")
// check if size fits quota // check if size fits quota
state.size += numBytes state.size += numBytes
val quota = plugin.getQuota() val quota = plugin.getQuota()
if (state.size > quota) { 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 TRANSPORT_QUOTA_EXCEEDED
} }
return try { return try {
// get output stream or initialize it, if it does not yet exist // 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 outputStream = state.outputStream ?: suspend {
val stream = state.outputStreamInit!!() // not-null due to check above val stream = state.outputStreamInit!!() // not-null due to check above
state.outputStream = stream state.outputStream = stream
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 // read backup data, encrypt it and write it to output stream
val payload = IOUtils.readFully(state.inputStream, numBytes) val payload = IOUtils.readFully(state.inputStream, numBytes)
@ -153,11 +166,11 @@ internal class FullBackup(
} }
@Throws(IOException::class) @Throws(IOException::class)
fun clearBackupData(packageInfo: PackageInfo) { suspend fun clearBackupData(packageInfo: PackageInfo) {
plugin.removeDataOfPackage(packageInfo) plugin.removeDataOfPackage(packageInfo)
} }
fun cancelFullBackup() { suspend fun cancelFullBackup() {
Log.i(TAG, "Cancel full backup") Log.i(TAG, "Cancel full backup")
val state = this.state ?: throw AssertionError("No state when canceling") val state = this.state ?: throw AssertionError("No state when canceling")
try { try {

View file

@ -16,6 +16,6 @@ interface FullBackupPlugin {
* Remove all data associated with the given package. * Remove all data associated with the given package.
*/ */
@Throws(IOException::class) @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") @Suppress("BlockingMethodInNonBlockingContext")
internal class KVBackup( internal class KVBackup(
private val plugin: KVBackupPlugin, private val plugin: KVBackupPlugin,
private val inputFactory: InputFactory, private val inputFactory: InputFactory,
private val headerWriter: HeaderWriter, private val headerWriter: HeaderWriter,
private val crypto: Crypto) { private val crypto: Crypto
) {
private var state: KVBackupState? = null private var state: KVBackupState? = null
@ -36,7 +37,11 @@ internal class KVBackup(
fun getQuota(): Long = plugin.getQuota() 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 isIncremental = flags and FLAG_INCREMENTAL != 0
val isNonIncremental = flags and FLAG_NON_INCREMENTAL != 0 val isNonIncremental = flags and FLAG_NON_INCREMENTAL != 0
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
@ -65,7 +70,10 @@ internal class KVBackup(
return backupError(TRANSPORT_ERROR) return backupError(TRANSPORT_ERROR)
} }
if (isIncremental && !hasDataForPackage) { 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) return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)
} }
@ -164,7 +172,7 @@ internal class KVBackup(
} }
@Throws(IOException::class) @Throws(IOException::class)
fun clearBackupData(packageInfo: PackageInfo) { suspend fun clearBackupData(packageInfo: PackageInfo) {
plugin.removeDataOfPackage(packageInfo) plugin.removeDataOfPackage(packageInfo)
} }
@ -179,18 +187,20 @@ internal class KVBackup(
* because [finishBackup] is not called when we don't return [TRANSPORT_OK]. * because [finishBackup] is not called when we don't return [TRANSPORT_OK].
*/ */
private fun backupError(result: Int): Int { 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 state = null
return result return result
} }
private class KVOperation( private class KVOperation(
internal val key: String, internal val key: String,
internal val base64Key: String, internal val base64Key: String,
/** /**
* value is null when this is a deletion operation * value is null when this is a deletion operation
*/ */
internal val value: ByteArray? internal val value: ByteArray?
) )
private sealed class Result<out T> { 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. * Return true if there are records stored for the given package.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun hasDataForPackage(packageInfo: PackageInfo): Boolean suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean
/** /**
* This marks the beginning of a backup operation. * 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. * Delete the record for the given package identified by the given key.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun deleteRecord(packageInfo: PackageInfo, key: String) suspend fun deleteRecord(packageInfo: PackageInfo, key: String)
/** /**
* Remove all data associated with the given package. * Remove all data associated with the given package.
*/ */
@Throws(IOException::class) @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 com.stevesoltys.seedvault.header.UnsupportedVersionException
import libcore.io.IoUtils.closeQuietly import libcore.io.IoUtils.closeQuietly
import java.io.IOException import java.io.IOException
import java.util.* import java.util.ArrayList
import javax.crypto.AEADBadTagException import javax.crypto.AEADBadTagException
private class KVRestoreState( private class KVRestoreState(
internal val token: Long, internal val token: Long,
internal val packageInfo: PackageInfo, internal val packageInfo: PackageInfo,
/** /**
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@ * Optional [PackageInfo] for single package restore, optimizes restore of @pm@
*/ */
internal val pmPackageInfo: PackageInfo?) internal val pmPackageInfo: PackageInfo?
)
private val TAG = KVRestore::class.java.simpleName private val TAG = KVRestore::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class KVRestore( internal class KVRestore(
private val plugin: KVRestorePlugin, private val plugin: KVRestorePlugin,
private val outputFactory: OutputFactory, private val outputFactory: OutputFactory,
private val headerReader: HeaderReader, private val headerReader: HeaderReader,
private val crypto: Crypto) { private val crypto: Crypto
) {
private var state: KVRestoreState? = null private var state: KVRestoreState? = null
@ -63,7 +66,7 @@ internal class KVRestore(
* @return One of [TRANSPORT_OK] * @return One of [TRANSPORT_OK]
* or [TRANSPORT_ERROR] (an error occurred, the restore should be aborted and rescheduled). * 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") val state = this.state ?: throw IllegalStateException("no state")
// The restore set is the concatenation of the individual record blobs, // 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)) for (recordKey in records) contents.add(DecodedKey(recordKey))
// remove keys that are not needed for single package @pm@ restore // remove keys that are not needed for single package @pm@ restore
val pmPackageName = state?.pmPackageInfo?.packageName val pmPackageName = state?.pmPackageInfo?.packageName
val sortedKeys = if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) { val sortedKeys =
val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName) if (packageInfo.packageName == MAGIC_PACKAGE_MANAGER && pmPackageName != null) {
Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName") val keys = listOf(ANCESTRAL_RECORD_KEY, GLOBAL_METADATA_KEY, pmPackageName)
contents.filterTo(ArrayList()) { it.key in keys } Log.d(TAG, "Single package restore, restrict restore keys to $pmPackageName")
} else contents contents.filterTo(ArrayList()) { it.key in keys }
} else contents
sortedKeys.sort() sortedKeys.sort()
return sortedKeys return sortedKeys
} }
@ -135,8 +139,13 @@ internal class KVRestore(
* Read the encrypted value for the given key and write it to the given [BackupDataOutput]. * Read the encrypted value for the given key and write it to the given [BackupDataOutput].
*/ */
@Throws(IOException::class, UnsupportedVersionException::class, SecurityException::class) @Throws(IOException::class, UnsupportedVersionException::class, SecurityException::class)
private fun readAndWriteValue(state: KVRestoreState, dKey: DecodedKey, out: BackupDataOutput) { private suspend fun readAndWriteValue(
val inputStream = plugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key) state: KVRestoreState,
dKey: DecodedKey,
out: BackupDataOutput
) {
val inputStream =
plugin.getInputStreamForRecord(state.token, state.packageInfo, dKey.base64Key)
try { try {
val version = headerReader.readVersion(inputStream) val version = headerReader.readVersion(inputStream)
crypto.decryptHeader(inputStream, version, state.packageInfo.packageName, dKey.key) 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. * Note: Implementations might expect that you call [hasDataForPackage] before.
*/ */
@Throws(IOException::class) @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 import java.io.IOException
private class RestoreCoordinatorState( private class RestoreCoordinatorState(
internal val token: Long, internal val token: Long,
internal val packages: Iterator<PackageInfo>, internal val packages: Iterator<PackageInfo>,
/** /**
* Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@ * Optional [PackageInfo] for single package restore, to reduce data needed to read for @pm@
*/ */
internal val pmPackageInfo: PackageInfo?) { internal val pmPackageInfo: PackageInfo?
) {
internal var currentPackage: String? = null internal var currentPackage: String? = null
} }
@ -39,14 +40,15 @@ private val TAG = RestoreCoordinator::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
internal class RestoreCoordinator( internal class RestoreCoordinator(
private val context: Context, private val context: Context,
private val settingsManager: SettingsManager, private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager, private val metadataManager: MetadataManager,
private val notificationManager: BackupNotificationManager, private val notificationManager: BackupNotificationManager,
private val plugin: RestorePlugin, private val plugin: RestorePlugin,
private val kv: KVRestore, private val kv: KVRestore,
private val full: FullRestore, private val full: FullRestore,
private val metadataReader: MetadataReader) { private val metadataReader: MetadataReader
) {
private var state: RestoreCoordinatorState? = null private var state: RestoreCoordinatorState? = null
private var backupMetadata: LongSparseArray<BackupMetadata>? = 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." "No error when getting encrypted metadata, but stream is still missing."
} }
try { try {
val metadata = metadataReader.readMetadata(encryptedMetadata.inputStream, encryptedMetadata.token) val metadata = metadataReader.readMetadata(
encryptedMetadata.inputStream,
encryptedMetadata.token
)
metadataMap.put(encryptedMetadata.token, metadata) metadataMap.put(encryptedMetadata.token, metadata)
val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token) val set = RestoreSet(metadata.deviceName, metadata.deviceName, metadata.token)
restoreSets.add(set) restoreSets.add(set)
@ -102,7 +107,7 @@ internal class RestoreCoordinator(
*/ */
fun getCurrentRestoreSet(): Long { fun getCurrentRestoreSet(): Long {
return metadataManager.getBackupToken() 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 }}") 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 // 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 pmPackageInfo =
val pmPackageName = packages[1].packageName if (packages.size == 2 && packages[0].packageName == MAGIC_PACKAGE_MANAGER) {
Log.d(TAG, "Optimize for single package restore of $pmPackageName") val pmPackageName = packages[1].packageName
// check if the backup is on removable storage that is not plugged in Log.d(TAG, "Optimize for single package restore of $pmPackageName")
if (isStorageRemovableAndNotAvailable()) { // check if the backup is on removable storage that is not plugged in
// check if we even have a backup of that app if (isStorageRemovableAndNotAvailable()) {
if (metadataManager.getPackageMetadata(pmPackageName) != null) { // check if we even have a backup of that app
// remind user to plug in storage device if (metadataManager.getPackageMetadata(pmPackageName) != null) {
val storageName = settingsManager.getStorage()?.name // remind user to plug in storage device
val storageName = settingsManager.getStorage()?.name
?: context.getString(R.string.settings_backup_location_none) ?: 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) state = RestoreCoordinatorState(token, packages.iterator(), pmPackageInfo)
failedPackages.clear() 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. * @param data An open, writable file into which the key/value backup data should be stored.
* @return the same error codes as [startRestore]. * @return the same error codes as [startRestore].
*/ */
fun getRestoreData(data: ParcelFileDescriptor): Int { suspend fun getRestoreData(data: ParcelFileDescriptor): Int {
return kv.getRestoreData(data).apply { return kv.getRestoreData(data).apply {
if (this != TRANSPORT_OK) { if (this != TRANSPORT_OK) {
// add current package to failed ones // add current package to failed ones

View file

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

View file

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

View file

@ -54,7 +54,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `incremental backup with no data gets rejected`() = runBlocking { 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)) assertEquals(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, backup.performBackup(packageInfo, data, FLAG_INCREMENTAL))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -62,7 +62,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `check for existing data throws exception`() = runBlocking { 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)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -71,7 +71,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `non-incremental backup with data clears old data first`() = runBlocking { fun `non-incremental backup with data clears old data first`() = runBlocking {
singleRecordBackup(true) singleRecordBackup(true)
every { plugin.removeDataOfPackage(packageInfo) } just Runs coEvery { plugin.removeDataOfPackage(packageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL)) assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL))
assertTrue(backup.hasState()) assertTrue(backup.hasState())
@ -82,7 +82,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `ignoring exception when clearing data when non-incremental backup has data`() = runBlocking { fun `ignoring exception when clearing data when non-incremental backup has data`() = runBlocking {
singleRecordBackup(true) singleRecordBackup(true)
every { plugin.removeDataOfPackage(packageInfo) } throws IOException() coEvery { plugin.removeDataOfPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL)) assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, FLAG_NON_INCREMENTAL))
assertTrue(backup.hasState()) assertTrue(backup.hasState())
@ -92,7 +92,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `ensuring storage throws exception`() = runBlocking { fun `ensuring storage throws exception`() = runBlocking {
every { plugin.hasDataForPackage(packageInfo) } returns false coEvery { plugin.hasDataForPackage(packageInfo) } returns false
coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException() coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
@ -194,7 +194,7 @@ internal class KVBackupTest : BackupTest() {
} }
private fun initPlugin(hasDataForPackage: Boolean = false) { private fun initPlugin(hasDataForPackage: Boolean = false) {
every { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage coEvery { plugin.hasDataForPackage(packageInfo) } returns hasDataForPackage
coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } just Runs 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.BackupDataOutput
import android.app.backup.BackupTransport.TRANSPORT_ERROR import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_OK
import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.UnsupportedVersionException
@ -16,7 +17,6 @@ import io.mockk.mockk
import io.mockk.verifyAll import io.mockk.verifyAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
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.Test import org.junit.jupiter.api.Test
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -47,13 +47,13 @@ internal class KVRestoreTest : RestoreTest() {
@Test @Test
fun `getRestoreData() throws without initializing state`() { fun `getRestoreData() throws without initializing state`() {
assertThrows(IllegalStateException::class.java) { coAssertThrows(IllegalStateException::class.java) {
restore.getRestoreData(fileDescriptor) restore.getRestoreData(fileDescriptor)
} }
} }
@Test @Test
fun `listing records throws`() { fun `listing records throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
every { plugin.listRecords(token, packageInfo) } throws IOException() every { plugin.listRecords(token, packageInfo) } throws IOException()
@ -62,11 +62,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `reading VersionHeader with unsupported version throws`() { fun `reading VersionHeader with unsupported version throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion) every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion)
streamsGetClosed() streamsGetClosed()
@ -75,11 +75,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `error reading VersionHeader throws`() { fun `error reading VersionHeader throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } throws IOException() every { headerReader.readVersion(inputStream) } throws IOException()
streamsGetClosed() streamsGetClosed()
@ -88,11 +88,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `decrypting segment throws`() { fun `decrypting segment throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } throws IOException() every { crypto.decryptMultipleSegments(inputStream) } throws IOException()
@ -103,11 +103,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `decrypting header throws`() { fun `decrypting header throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws IOException() every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws IOException()
streamsGetClosed() streamsGetClosed()
@ -117,11 +117,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `decrypting header throws security exception`() { fun `decrypting header throws security exception`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws SecurityException() every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws SecurityException()
streamsGetClosed() streamsGetClosed()
@ -131,11 +131,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `writing header throws`() { fun `writing header throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data every { crypto.decryptMultipleSegments(inputStream) } returns data
@ -147,11 +147,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `writing value throws`() { fun `writing value throws`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data every { crypto.decryptMultipleSegments(inputStream) } returns data
@ -164,11 +164,11 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `writing value succeeds`() { fun `writing value succeeds`() = runBlocking {
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput() getRecordsAndOutput()
every { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
every { headerReader.readVersion(inputStream) } returns VERSION every { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data every { crypto.decryptMultipleSegments(inputStream) } returns data
@ -181,21 +181,21 @@ internal class KVRestoreTest : RestoreTest() {
} }
@Test @Test
fun `writing two values succeeds`() { fun `writing two values succeeds`() = runBlocking {
val data2 = getRandomByteArray() val data2 = getRandomByteArray()
val inputStream2 = mockk<InputStream>() val inputStream2 = mockk<InputStream>()
restore.initializeState(token, packageInfo) restore.initializeState(token, packageInfo)
getRecordsAndOutput(listOf(key64, key264)) getRecordsAndOutput(listOf(key64, key264))
// first key/value // 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 { headerReader.readVersion(inputStream) } returns VERSION
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
every { crypto.decryptMultipleSegments(inputStream) } returns data every { crypto.decryptMultipleSegments(inputStream) } returns data
every { output.writeEntityHeader(key, data.size) } returns 42 every { output.writeEntityHeader(key, data.size) } returns 42
every { output.writeEntityData(data, data.size) } returns data.size every { output.writeEntityData(data, data.size) } returns data.size
// second key/value // 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 { headerReader.readVersion(inputStream2) } returns VERSION
every { crypto.decryptHeader(inputStream2, VERSION, packageInfo.packageName, key2) } returns versionHeader2 every { crypto.decryptHeader(inputStream2, VERSION, packageInfo.packageName, key2) } returns versionHeader2
every { crypto.decryptMultipleSegments(inputStream2) } returns data2 every { crypto.decryptMultipleSegments(inputStream2) } returns data2

View file

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