Replace all instances of DocumentFile#findFile with #findFileBlocking
Also start sticking closer to the official Kotlin formatting style
This commit is contained in:
parent
18d83767b3
commit
2958c8fac8
22 changed files with 561 additions and 287 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue