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"
|
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()
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue