Let ApkBackup and ApkRestore use the new storage plugin API
This commit is contained in:
parent
183e34afd2
commit
50066f0317
12 changed files with 335 additions and 61 deletions
|
@ -44,6 +44,10 @@ internal interface Crypto {
|
||||||
|
|
||||||
fun getNameForPackage(salt: String, packageName: String): String
|
fun getNameForPackage(salt: String, packageName: String): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name that identifies an APK in the backup storage plugin.
|
||||||
|
* @param suffix empty string for normal APKs and the name of the split in case of an APK split
|
||||||
|
*/
|
||||||
fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
|
fun getNameForApk(salt: String, packageName: String, suffix: String = ""): String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -8,9 +8,15 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) {
|
||||||
val name: String
|
val name: String
|
||||||
get() = backupMetadata.deviceName
|
get() = backupMetadata.deviceName
|
||||||
|
|
||||||
|
val version: Byte
|
||||||
|
get() = backupMetadata.version
|
||||||
|
|
||||||
val token: Long
|
val token: Long
|
||||||
get() = backupMetadata.token
|
get() = backupMetadata.token
|
||||||
|
|
||||||
|
val salt: String
|
||||||
|
get() = backupMetadata.salt
|
||||||
|
|
||||||
val time: Long
|
val time: Long
|
||||||
get() = backupMetadata.time
|
get() = backupMetadata.time
|
||||||
|
|
||||||
|
|
|
@ -122,6 +122,7 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
@Throws(RemoteException::class)
|
@Throws(RemoteException::class)
|
||||||
private fun getOrStartSession(): IRestoreSession {
|
private fun getOrStartSession(): IRestoreSession {
|
||||||
|
@Suppress("UNRESOLVED_REFERENCE")
|
||||||
val session = this.session
|
val session = this.session
|
||||||
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
|
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
|
||||||
?: throw RemoteException("beginRestoreSessionForUser returned null")
|
?: throw RemoteException("beginRestoreSessionForUser returned null")
|
||||||
|
@ -155,7 +156,7 @@ internal class RestoreViewModel(
|
||||||
|
|
||||||
private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
|
private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
|
||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
return apkRestore.restore(backup.token, backup.deviceName, backup.packageMetadataMap)
|
return apkRestore.restore(backup)
|
||||||
.onStart {
|
.onStart {
|
||||||
Log.d(TAG, "Start InstallResult Flow")
|
Log.d(TAG, "Start InstallResult Flow")
|
||||||
}.catch { e ->
|
}.catch { e ->
|
||||||
|
|
|
@ -5,13 +5,15 @@ import android.content.pm.PackageManager
|
||||||
import android.content.pm.PackageManager.GET_SIGNATURES
|
import android.content.pm.PackageManager.GET_SIGNATURES
|
||||||
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash
|
import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash
|
||||||
import com.stevesoltys.seedvault.transport.backup.getSignatures
|
import com.stevesoltys.seedvault.transport.backup.getSignatures
|
||||||
import com.stevesoltys.seedvault.transport.backup.isSystemApp
|
import com.stevesoltys.seedvault.transport.backup.isSystemApp
|
||||||
|
@ -26,16 +28,19 @@ private val TAG = ApkRestore::class.java.simpleName
|
||||||
|
|
||||||
internal class ApkRestore(
|
internal class ApkRestore(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
private val backupPlugin: BackupPlugin,
|
||||||
private val restorePlugin: RestorePlugin,
|
private val restorePlugin: RestorePlugin,
|
||||||
|
private val crypto: Crypto,
|
||||||
private val splitCompatChecker: ApkSplitCompatibilityChecker,
|
private val splitCompatChecker: ApkSplitCompatibilityChecker,
|
||||||
private val apkInstaller: ApkInstaller
|
private val apkInstaller: ApkInstaller
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val pm = context.packageManager
|
private val pm = context.packageManager
|
||||||
|
|
||||||
fun restore(token: Long, deviceName: String, packageMetadataMap: PackageMetadataMap) = flow {
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
fun restore(backup: RestorableBackup) = flow {
|
||||||
// filter out packages without APK and get total
|
// filter out packages without APK and get total
|
||||||
val packages = packageMetadataMap.filter { it.value.hasApk() }
|
val packages = backup.packageMetadataMap.filter { it.value.hasApk() }
|
||||||
val total = packages.size
|
val total = packages.size
|
||||||
var progress = 0
|
var progress = 0
|
||||||
|
|
||||||
|
@ -55,7 +60,7 @@ internal class ApkRestore(
|
||||||
// re-install individual packages and emit updates
|
// re-install individual packages and emit updates
|
||||||
for ((packageName, metadata) in packages) {
|
for ((packageName, metadata) in packages) {
|
||||||
try {
|
try {
|
||||||
restore(this, token, deviceName, packageName, metadata, installResult)
|
restore(this, backup, packageName, metadata, installResult)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Error re-installing APK for $packageName.", e)
|
Log.e(TAG, "Error re-installing APK for $packageName.", e)
|
||||||
emit(installResult.fail(packageName))
|
emit(installResult.fail(packageName))
|
||||||
|
@ -75,14 +80,13 @@ internal class ApkRestore(
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
private suspend fun restore(
|
private suspend fun restore(
|
||||||
collector: FlowCollector<InstallResult>,
|
collector: FlowCollector<InstallResult>,
|
||||||
token: Long,
|
backup: RestorableBackup,
|
||||||
deviceName: String,
|
|
||||||
packageName: String,
|
packageName: String,
|
||||||
metadata: PackageMetadata,
|
metadata: PackageMetadata,
|
||||||
installResult: MutableInstallResult
|
installResult: MutableInstallResult
|
||||||
) {
|
) {
|
||||||
// cache the APK and get its hash
|
// cache the APK and get its hash
|
||||||
val (cachedApk, sha256) = cacheApk(token, packageName)
|
val (cachedApk, sha256) = cacheApk(backup.version, backup.token, backup.salt, packageName)
|
||||||
|
|
||||||
// check APK's SHA-256 hash
|
// check APK's SHA-256 hash
|
||||||
if (metadata.sha256 != sha256) throw SecurityException(
|
if (metadata.sha256 != sha256) throw SecurityException(
|
||||||
|
@ -139,7 +143,7 @@ internal class ApkRestore(
|
||||||
|
|
||||||
// process further APK splits, if available
|
// process further APK splits, if available
|
||||||
val cachedApks =
|
val cachedApks =
|
||||||
cacheSplitsIfNeeded(token, deviceName, packageName, cachedApk, metadata.splits)
|
cacheSplitsIfNeeded(backup, packageName, cachedApk, metadata.splits)
|
||||||
if (cachedApks == null) {
|
if (cachedApks == null) {
|
||||||
Log.w(TAG, "Not installing $packageName because of incompatible splits.")
|
Log.w(TAG, "Not installing $packageName because of incompatible splits.")
|
||||||
collector.emit(installResult.fail(packageName))
|
collector.emit(installResult.fail(packageName))
|
||||||
|
@ -161,8 +165,7 @@ internal class ApkRestore(
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
private suspend fun cacheSplitsIfNeeded(
|
private suspend fun cacheSplitsIfNeeded(
|
||||||
token: Long,
|
backup: RestorableBackup,
|
||||||
deviceName: String,
|
|
||||||
packageName: String,
|
packageName: String,
|
||||||
cachedApk: File,
|
cachedApk: File,
|
||||||
splits: List<ApkSplit>?
|
splits: List<ApkSplit>?
|
||||||
|
@ -171,15 +174,16 @@ internal class ApkRestore(
|
||||||
val splitNames = splits?.map { it.name } ?: return listOf(cachedApk)
|
val splitNames = splits?.map { it.name } ?: return listOf(cachedApk)
|
||||||
|
|
||||||
// return null when splits are incompatible
|
// return null when splits are incompatible
|
||||||
if (!splitCompatChecker.isCompatible(deviceName, splitNames)) return null
|
if (!splitCompatChecker.isCompatible(backup.deviceName, splitNames)) return null
|
||||||
|
|
||||||
// store coming splits in a list
|
// store coming splits in a list
|
||||||
val cachedApks = ArrayList<File>(splits.size + 1).apply {
|
val cachedApks = ArrayList<File>(splits.size + 1).apply {
|
||||||
add(cachedApk) // don't forget the base APK
|
add(cachedApk) // don't forget the base APK
|
||||||
}
|
}
|
||||||
splits.forEach { apkSplit -> // cache and check all splits
|
splits.forEach { apkSplit -> // cache and check all splits
|
||||||
val suffix = "_${apkSplit.sha256}"
|
val suffix = if (backup.version == 0.toByte()) "_${apkSplit.sha256}" else apkSplit.name
|
||||||
val (file, sha256) = cacheApk(token, packageName, suffix)
|
val salt = backup.salt
|
||||||
|
val (file, sha256) = cacheApk(backup.version, backup.token, salt, packageName, suffix)
|
||||||
// check APK split's SHA-256 hash
|
// check APK split's SHA-256 hash
|
||||||
if (apkSplit.sha256 != sha256) throw SecurityException(
|
if (apkSplit.sha256 != sha256) throw SecurityException(
|
||||||
"$packageName:${apkSplit.name} has sha256 '$sha256'," +
|
"$packageName:${apkSplit.name} has sha256 '$sha256'," +
|
||||||
|
@ -199,14 +203,22 @@ internal class ApkRestore(
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
||||||
private suspend fun cacheApk(
|
private suspend fun cacheApk(
|
||||||
|
version: Byte,
|
||||||
token: Long,
|
token: Long,
|
||||||
|
salt: String,
|
||||||
packageName: String,
|
packageName: String,
|
||||||
suffix: String = ""
|
suffix: String = ""
|
||||||
): Pair<File, String> {
|
): Pair<File, String> {
|
||||||
// create a cache file to write the APK into
|
// create a cache file to write the APK into
|
||||||
val cachedApk = File.createTempFile(packageName + suffix, ".apk", context.cacheDir)
|
val cachedApk = File.createTempFile(packageName + suffix, ".apk", context.cacheDir)
|
||||||
// copy APK to cache file and calculate SHA-256 hash while we are at it
|
// copy APK to cache file and calculate SHA-256 hash while we are at it
|
||||||
val inputStream = restorePlugin.getApkInputStream(token, packageName, suffix)
|
val inputStream = if (version == 0.toByte()) {
|
||||||
|
@Suppress("Deprecation")
|
||||||
|
restorePlugin.getApkInputStream(token, packageName, suffix)
|
||||||
|
} else {
|
||||||
|
val name = crypto.getNameForApk(salt, packageName, suffix)
|
||||||
|
backupPlugin.getInputStream(token, name)
|
||||||
|
}
|
||||||
val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream())
|
val sha256 = copyStreamsAndGetHash(inputStream, cachedApk.outputStream())
|
||||||
return Pair(cachedApk, sha256)
|
return Pair(cachedApk, sha256)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,5 @@ val installModule = module {
|
||||||
factory { ApkInstaller(androidContext()) }
|
factory { ApkInstaller(androidContext()) }
|
||||||
factory { DeviceInfo(androidContext()) }
|
factory { DeviceInfo(androidContext()) }
|
||||||
factory { ApkSplitCompatibilityChecker(get()) }
|
factory { ApkSplitCompatibilityChecker(get()) }
|
||||||
factory { ApkRestore(androidContext(), get(), get(), get()) }
|
factory { ApkRestore(androidContext(), get(), get(), get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.content.pm.SigningInfo
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.PackageUtils.computeSha256DigestBytes
|
import android.util.PackageUtils.computeSha256DigestBytes
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
|
import com.stevesoltys.seedvault.crypto.Crypto
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
import com.stevesoltys.seedvault.encodeBase64
|
||||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
|
@ -27,6 +28,7 @@ private val TAG = ApkBackup::class.java.simpleName
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class ApkBackup(
|
internal class ApkBackup(
|
||||||
private val pm: PackageManager,
|
private val pm: PackageManager,
|
||||||
|
private val crypto: Crypto,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val metadataManager: MetadataManager
|
private val metadataManager: MetadataManager
|
||||||
) {
|
) {
|
||||||
|
@ -44,7 +46,7 @@ internal class ApkBackup(
|
||||||
suspend fun backupApkIfNecessary(
|
suspend fun backupApkIfNecessary(
|
||||||
packageInfo: PackageInfo,
|
packageInfo: PackageInfo,
|
||||||
packageState: PackageState,
|
packageState: PackageState,
|
||||||
streamGetter: suspend (suffix: String) -> OutputStream
|
streamGetter: suspend (name: String) -> OutputStream
|
||||||
): PackageMetadata? {
|
): PackageMetadata? {
|
||||||
// do not back up @pm@
|
// do not back up @pm@
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
|
@ -102,7 +104,8 @@ internal class ApkBackup(
|
||||||
// get an InputStream for the APK
|
// get an InputStream for the APK
|
||||||
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
|
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
|
||||||
// copy the APK to the storage's output and calculate SHA-256 hash while at it
|
// copy the APK to the storage's output and calculate SHA-256 hash while at it
|
||||||
val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(""))
|
val name = crypto.getNameForApk(metadataManager.salt, packageName)
|
||||||
|
val sha256 = copyStreamsAndGetHash(inputStream, streamGetter(name))
|
||||||
|
|
||||||
// back up splits if they exist
|
// back up splits if they exist
|
||||||
val splits =
|
val splits =
|
||||||
|
@ -148,7 +151,7 @@ internal class ApkBackup(
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private suspend fun backupSplitApks(
|
private suspend fun backupSplitApks(
|
||||||
packageInfo: PackageInfo,
|
packageInfo: PackageInfo,
|
||||||
streamGetter: suspend (suffix: String) -> OutputStream
|
streamGetter: suspend (name: String) -> OutputStream
|
||||||
): List<ApkSplit> {
|
): List<ApkSplit> {
|
||||||
check(packageInfo.splitNames != null)
|
check(packageInfo.splitNames != null)
|
||||||
val splitSourceDirs = packageInfo.applicationInfo.splitSourceDirs
|
val splitSourceDirs = packageInfo.applicationInfo.splitSourceDirs
|
||||||
|
@ -159,7 +162,12 @@ internal class ApkBackup(
|
||||||
}
|
}
|
||||||
val splits = ArrayList<ApkSplit>(packageInfo.splitNames.size)
|
val splits = ArrayList<ApkSplit>(packageInfo.splitNames.size)
|
||||||
for (i in packageInfo.splitNames.indices) {
|
for (i in packageInfo.splitNames.indices) {
|
||||||
val split = backupSplitApk(packageInfo.splitNames[i], splitSourceDirs[i], streamGetter)
|
val split = backupSplitApk(
|
||||||
|
packageName = packageInfo.packageName,
|
||||||
|
splitName = packageInfo.splitNames[i],
|
||||||
|
sourceDir = splitSourceDirs[i],
|
||||||
|
streamGetter = streamGetter
|
||||||
|
)
|
||||||
splits.add(split)
|
splits.add(split)
|
||||||
}
|
}
|
||||||
return splits
|
return splits
|
||||||
|
@ -167,9 +175,10 @@ internal class ApkBackup(
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private suspend fun backupSplitApk(
|
private suspend fun backupSplitApk(
|
||||||
name: String,
|
packageName: String,
|
||||||
|
splitName: String,
|
||||||
sourceDir: String,
|
sourceDir: String,
|
||||||
streamGetter: suspend (suffix: String) -> OutputStream
|
streamGetter: suspend (name: String) -> OutputStream
|
||||||
): ApkSplit {
|
): ApkSplit {
|
||||||
// Calculate sha256 hash first to determine file name suffix.
|
// Calculate sha256 hash first to determine file name suffix.
|
||||||
// We could also just use the split name as a suffix, but there is a theoretical risk
|
// We could also just use the split name as a suffix, but there is a theoretical risk
|
||||||
|
@ -185,14 +194,14 @@ internal class ApkBackup(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val sha256 = messageDigest.digest().encodeBase64()
|
val sha256 = messageDigest.digest().encodeBase64()
|
||||||
val suffix = "_$sha256"
|
val name = crypto.getNameForApk(metadataManager.salt, packageName, splitName)
|
||||||
// copy the split APK to the storage stream
|
// copy the split APK to the storage stream
|
||||||
getApkInputStream(sourceDir).use { inputStream ->
|
getApkInputStream(sourceDir).use { inputStream ->
|
||||||
streamGetter(suffix).use { outputStream ->
|
streamGetter(name).use { outputStream ->
|
||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ApkSplit(name, sha256)
|
return ApkSplit(splitName, sha256)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -453,9 +453,8 @@ internal class BackupCoordinator(
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
return try {
|
return try {
|
||||||
apkBackup.backupApkIfNecessary(packageInfo, packageState) { suffix ->
|
apkBackup.backupApkIfNecessary(packageInfo, packageState) { name ->
|
||||||
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
val token = settingsManager.getToken() ?: throw IOException("no current token")
|
||||||
val name = "${packageInfo.packageName}$suffix.apk"
|
|
||||||
plugin.getOutputStream(token, name)
|
plugin.getOutputStream(token, name)
|
||||||
}?.let { packageMetadata ->
|
}?.let { packageMetadata ->
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
|
|
|
@ -14,6 +14,7 @@ val backupModule = module {
|
||||||
single {
|
single {
|
||||||
ApkBackup(
|
ApkBackup(
|
||||||
pm = androidContext().packageManager,
|
pm = androidContext().packageManager,
|
||||||
|
crypto = get(),
|
||||||
settingsManager = get(),
|
settingsManager = get(),
|
||||||
metadataManager = get()
|
metadataManager = get()
|
||||||
)
|
)
|
||||||
|
|
|
@ -13,6 +13,7 @@ interface RestorePlugin {
|
||||||
* Returns an [InputStream] for the given token, for reading an APK that is to be restored.
|
* Returns an [InputStream] for the given token, for reading an APK that is to be restored.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
|
@Deprecated("Use only for v0 restores")
|
||||||
suspend fun getApkInputStream(token: Long, packageName: String, suffix: String): InputStream
|
suspend fun getApkInputStream(token: Long, packageName: String, suffix: String): InputStream
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
package com.stevesoltys.seedvault.restore.install
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.content.pm.Signature
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.PackageUtils
|
||||||
|
import com.stevesoltys.seedvault.assertReadEquals
|
||||||
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
|
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageState
|
||||||
|
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||||
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.ApkBackup
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
|
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.slot
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.collectIndexed
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.io.TempDir
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
internal class ApkBackupRestoreTest : TransportTest() {
|
||||||
|
|
||||||
|
private val pm: PackageManager = mockk()
|
||||||
|
private val strictContext: Context = mockk<Context>().apply {
|
||||||
|
every { packageManager } returns pm
|
||||||
|
}
|
||||||
|
private val backupPlugin: BackupPlugin = mockk()
|
||||||
|
private val restorePlugin: RestorePlugin = mockk()
|
||||||
|
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||||
|
private val apkInstaller: ApkInstaller = mockk()
|
||||||
|
|
||||||
|
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
|
||||||
|
private val apkRestore: ApkRestore = ApkRestore(
|
||||||
|
context = strictContext,
|
||||||
|
backupPlugin = backupPlugin,
|
||||||
|
restorePlugin = restorePlugin,
|
||||||
|
crypto = crypto,
|
||||||
|
splitCompatChecker = splitCompatChecker,
|
||||||
|
apkInstaller = apkInstaller
|
||||||
|
)
|
||||||
|
|
||||||
|
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||||
|
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
||||||
|
private val sigs = arrayOf(Signature(signatureBytes))
|
||||||
|
private val packageName: String = packageInfo.packageName
|
||||||
|
private val splitName = getRandomString()
|
||||||
|
private val splitBytes = byteArrayOf(0x07, 0x08, 0x09)
|
||||||
|
private val splitSha256 = "ZqZ1cVH47lXbEncWx-Pc4L6AdLZOIO2lQuXB5GypxB4"
|
||||||
|
private val packageMetadata = PackageMetadata(
|
||||||
|
time = Random.nextLong(),
|
||||||
|
version = packageInfo.longVersionCode - 1,
|
||||||
|
installer = getRandomString(),
|
||||||
|
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
||||||
|
signatures = listOf("AwIB"),
|
||||||
|
splits = listOf(ApkSplit(splitName, splitSha256))
|
||||||
|
)
|
||||||
|
private val packageMetadataMap: PackageMetadataMap = hashMapOf(packageName to packageMetadata)
|
||||||
|
private val installerName = packageMetadata.installer
|
||||||
|
private val icon: Drawable = mockk()
|
||||||
|
private val appName = getRandomString()
|
||||||
|
private val suffixName = getRandomString()
|
||||||
|
private val outputStream = ByteArrayOutputStream()
|
||||||
|
private val splitOutputStream = ByteArrayOutputStream()
|
||||||
|
private val outputStreamGetter: suspend (name: String) -> OutputStream = { name ->
|
||||||
|
if (name == this.name) outputStream else splitOutputStream
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
mockkStatic(PackageUtils::class)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test backup and restore with a split`(@TempDir tmpDir: Path) = runBlocking {
|
||||||
|
val apkBytes = byteArrayOf(0x04, 0x05, 0x06)
|
||||||
|
val tmpFile = File(tmpDir.toAbsolutePath().toString())
|
||||||
|
packageInfo.applicationInfo.sourceDir = File(tmpFile, "test.apk").apply {
|
||||||
|
assertTrue(createNewFile())
|
||||||
|
writeBytes(apkBytes)
|
||||||
|
}.absolutePath
|
||||||
|
packageInfo.splitNames = arrayOf(splitName)
|
||||||
|
packageInfo.applicationInfo.splitSourceDirs = arrayOf(File(tmpFile, "split.apk").apply {
|
||||||
|
assertTrue(createNewFile())
|
||||||
|
writeBytes(splitBytes)
|
||||||
|
}.absolutePath)
|
||||||
|
|
||||||
|
every { settingsManager.backupApks() } returns true
|
||||||
|
every { sigInfo.hasMultipleSigners() } returns false
|
||||||
|
every { sigInfo.signingCertificateHistory } returns sigs
|
||||||
|
every { PackageUtils.computeSha256DigestBytes(signatureBytes) } returns signatureHash
|
||||||
|
every {
|
||||||
|
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||||
|
} returns packageMetadata
|
||||||
|
every { pm.getInstallSourceInfo(packageInfo.packageName) } returns mockk(relaxed = true)
|
||||||
|
every { metadataManager.salt } returns salt
|
||||||
|
every { crypto.getNameForApk(salt, packageName) } returns name
|
||||||
|
every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
|
||||||
|
|
||||||
|
apkBackup.backupApkIfNecessary(packageInfo, PackageState.APK_AND_DATA, outputStreamGetter)
|
||||||
|
|
||||||
|
assertArrayEquals(apkBytes, outputStream.toByteArray())
|
||||||
|
assertArrayEquals(splitBytes, splitOutputStream.toByteArray())
|
||||||
|
|
||||||
|
val inputStream = ByteArrayInputStream(apkBytes)
|
||||||
|
val splitInputStream = ByteArrayInputStream(splitBytes)
|
||||||
|
val apkPath = slot<String>()
|
||||||
|
val cacheFiles = slot<List<File>>()
|
||||||
|
|
||||||
|
every { strictContext.cacheDir } returns tmpFile
|
||||||
|
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||||
|
coEvery { backupPlugin.getInputStream(token, name) } returns inputStream
|
||||||
|
every { pm.getPackageArchiveInfo(capture(apkPath), any()) } returns packageInfo
|
||||||
|
every {
|
||||||
|
@Suppress("UNRESOLVED_REFERENCE")
|
||||||
|
pm.loadItemIcon(
|
||||||
|
packageInfo.applicationInfo,
|
||||||
|
packageInfo.applicationInfo
|
||||||
|
)
|
||||||
|
} returns icon
|
||||||
|
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
||||||
|
every {
|
||||||
|
splitCompatChecker.isCompatible(metadata.deviceName, listOf(splitName))
|
||||||
|
} returns true
|
||||||
|
every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
|
||||||
|
coEvery { backupPlugin.getInputStream(token, suffixName) } returns splitInputStream
|
||||||
|
coEvery {
|
||||||
|
apkInstaller.install(capture(cacheFiles), packageName, installerName, any())
|
||||||
|
} returns MutableInstallResult(1).apply {
|
||||||
|
set(
|
||||||
|
packageName, ApkInstallResult(
|
||||||
|
packageName,
|
||||||
|
progress = 1,
|
||||||
|
state = ApkInstallState.SUCCEEDED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val backup = RestorableBackup(metadata.copy(packageMetadataMap = packageMetadataMap))
|
||||||
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
|
assertFalse(value.hasFailed)
|
||||||
|
assertEquals(1, value.total)
|
||||||
|
if (i == 3) assertTrue(value.isFinished)
|
||||||
|
}
|
||||||
|
|
||||||
|
val apkFile = File(apkPath.captured)
|
||||||
|
assertEquals(2, cacheFiles.captured.size)
|
||||||
|
assertEquals(apkFile, cacheFiles.captured[0])
|
||||||
|
val splitFile = cacheFiles.captured[1]
|
||||||
|
assertReadEquals(apkBytes, FileInputStream(apkFile))
|
||||||
|
assertReadEquals(splitBytes, FileInputStream(splitFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -14,12 +14,14 @@ import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.ApkSplit
|
import com.stevesoltys.seedvault.metadata.ApkSplit
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
|
import com.stevesoltys.seedvault.restore.RestorableBackup
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED_SYSTEM_APP
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
|
||||||
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
|
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
|
||||||
import com.stevesoltys.seedvault.transport.TransportTest
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
|
||||||
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
@ -48,16 +50,23 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
private val strictContext: Context = mockk<Context>().apply {
|
private val strictContext: Context = mockk<Context>().apply {
|
||||||
every { packageManager } returns pm
|
every { packageManager } returns pm
|
||||||
}
|
}
|
||||||
|
private val backupPlugin: BackupPlugin = mockk()
|
||||||
private val restorePlugin: RestorePlugin = mockk()
|
private val restorePlugin: RestorePlugin = mockk()
|
||||||
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
private val splitCompatChecker: ApkSplitCompatibilityChecker = mockk()
|
||||||
private val apkInstaller: ApkInstaller = mockk()
|
private val apkInstaller: ApkInstaller = mockk()
|
||||||
|
|
||||||
private val apkRestore: ApkRestore =
|
private val apkRestore: ApkRestore = ApkRestore(
|
||||||
ApkRestore(strictContext, restorePlugin, splitCompatChecker, apkInstaller)
|
strictContext,
|
||||||
|
backupPlugin,
|
||||||
|
restorePlugin,
|
||||||
|
crypto,
|
||||||
|
splitCompatChecker,
|
||||||
|
apkInstaller
|
||||||
|
)
|
||||||
|
|
||||||
private val icon: Drawable = mockk()
|
private val icon: Drawable = mockk()
|
||||||
|
|
||||||
private val deviceName = getRandomString()
|
private val deviceName = metadata.deviceName
|
||||||
private val packageName = packageInfo.packageName
|
private val packageName = packageInfo.packageName
|
||||||
private val packageMetadata = PackageMetadata(
|
private val packageMetadata = PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
|
@ -71,6 +80,8 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
private val apkInputStream = ByteArrayInputStream(apkBytes)
|
private val apkInputStream = ByteArrayInputStream(apkBytes)
|
||||||
private val appName = getRandomString()
|
private val appName = getRandomString()
|
||||||
private val installerName = packageMetadata.installer
|
private val installerName = packageMetadata.installer
|
||||||
|
private val backup = RestorableBackup(metadata.copy(packageMetadataMap = packageMetadataMap))
|
||||||
|
private val suffixName = getRandomString()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// as we don't do strict signature checking, we can use a relaxed mock
|
// as we don't do strict signature checking, we can use a relaxed mock
|
||||||
|
@ -81,12 +92,13 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
fun `signature mismatch causes FAILED status`(@TempDir tmpDir: Path) = runBlocking {
|
fun `signature mismatch causes FAILED status`(@TempDir tmpDir: Path) = runBlocking {
|
||||||
// change SHA256 signature to random
|
// change SHA256 signature to random
|
||||||
val packageMetadata = packageMetadata.copy(sha256 = getRandomString())
|
val packageMetadata = packageMetadata.copy(sha256 = getRandomString())
|
||||||
val packageMetadataMap: PackageMetadataMap = hashMapOf(packageName to packageMetadata)
|
val backup = swapPackages(hashMapOf(packageName to packageMetadata))
|
||||||
|
|
||||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
coEvery { restorePlugin.getApkInputStream(token, packageName, "") } returns apkInputStream
|
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||||
|
coEvery { backupPlugin.getInputStream(token, name) } returns apkInputStream
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedFailFinished(i, value)
|
assertQueuedFailFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,10 +109,11 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
packageInfo.packageName = getRandomString()
|
packageInfo.packageName = getRandomString()
|
||||||
|
|
||||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
coEvery { restorePlugin.getApkInputStream(token, packageName, "") } returns apkInputStream
|
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||||
|
coEvery { backupPlugin.getInputStream(token, name) } returns apkInputStream
|
||||||
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedFailFinished(i, value)
|
assertQueuedFailFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,7 +125,7 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||||
} throws SecurityException()
|
} throws SecurityException()
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedProgressFailFinished(i, value)
|
assertQueuedProgressFailFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,7 +147,43 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||||
} returns installResult
|
} returns installResult
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
|
assertQueuedProgressSuccessFinished(i, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `v0 test successful run`(@TempDir tmpDir: Path) = runBlocking {
|
||||||
|
// This is a legacy backup with version 0
|
||||||
|
val backup = backup.copy(backupMetadata = backup.backupMetadata.copy(version = 0))
|
||||||
|
// Install will be successful
|
||||||
|
val installResult = MutableInstallResult(1).apply {
|
||||||
|
set(
|
||||||
|
packageName, ApkInstallResult(
|
||||||
|
packageName,
|
||||||
|
progress = 1,
|
||||||
|
state = SUCCEEDED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
|
@Suppress("Deprecation")
|
||||||
|
coEvery { restorePlugin.getApkInputStream(token, packageName, "") } returns apkInputStream
|
||||||
|
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||||
|
every {
|
||||||
|
@Suppress("UNRESOLVED_REFERENCE")
|
||||||
|
pm.loadItemIcon(
|
||||||
|
packageInfo.applicationInfo,
|
||||||
|
packageInfo.applicationInfo
|
||||||
|
)
|
||||||
|
} returns icon
|
||||||
|
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
||||||
|
coEvery {
|
||||||
|
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
|
||||||
|
} returns installResult
|
||||||
|
|
||||||
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedProgressSuccessFinished(i, value)
|
assertQueuedProgressSuccessFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,7 +230,7 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> {
|
0 -> {
|
||||||
val result = value[packageName]
|
val result = value[packageName]
|
||||||
|
@ -231,7 +280,7 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
splitCompatChecker.isCompatible(deviceName, listOf(split1Name, split2Name))
|
splitCompatChecker.isCompatible(deviceName, listOf(split1Name, split2Name))
|
||||||
} returns false
|
} returns false
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedProgressFailFinished(i, value)
|
assertQueuedProgressFailFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -240,20 +289,20 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
fun `split signature mismatch causes FAILED state`(@TempDir tmpDir: Path) = runBlocking {
|
fun `split signature mismatch causes FAILED state`(@TempDir tmpDir: Path) = runBlocking {
|
||||||
// add one APK split to metadata
|
// add one APK split to metadata
|
||||||
val splitName = getRandomString()
|
val splitName = getRandomString()
|
||||||
val sha256 = getRandomBase64(23)
|
|
||||||
packageMetadataMap[packageName] = packageMetadataMap[packageName]!!.copy(
|
packageMetadataMap[packageName] = packageMetadataMap[packageName]!!.copy(
|
||||||
splits = listOf(ApkSplit(splitName, sha256))
|
splits = listOf(ApkSplit(splitName, getRandomBase64(23)))
|
||||||
)
|
)
|
||||||
|
|
||||||
// cache APK and get icon as well as app name
|
// cache APK and get icon as well as app name
|
||||||
cacheBaseApkAndGetInfo(tmpDir)
|
cacheBaseApkAndGetInfo(tmpDir)
|
||||||
|
|
||||||
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
|
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
|
||||||
|
every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
|
||||||
coEvery {
|
coEvery {
|
||||||
restorePlugin.getApkInputStream(token, packageName, "_$sha256")
|
backupPlugin.getInputStream(token, suffixName)
|
||||||
} returns ByteArrayInputStream(getRandomByteArray())
|
} returns ByteArrayInputStream(getRandomByteArray())
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedProgressFailFinished(i, value)
|
assertQueuedProgressFailFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -272,11 +321,10 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
cacheBaseApkAndGetInfo(tmpDir)
|
cacheBaseApkAndGetInfo(tmpDir)
|
||||||
|
|
||||||
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
|
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
|
||||||
coEvery {
|
every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
|
||||||
restorePlugin.getApkInputStream(token, packageName, "_$sha256")
|
coEvery { backupPlugin.getInputStream(token, suffixName) } throws IOException()
|
||||||
} throws IOException()
|
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedProgressFailFinished(i, value)
|
assertQueuedProgressFailFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -307,12 +355,12 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
val split2Bytes = byteArrayOf(0x07, 0x08, 0x09)
|
val split2Bytes = byteArrayOf(0x07, 0x08, 0x09)
|
||||||
val split1InputStream = ByteArrayInputStream(split1Bytes)
|
val split1InputStream = ByteArrayInputStream(split1Bytes)
|
||||||
val split2InputStream = ByteArrayInputStream(split2Bytes)
|
val split2InputStream = ByteArrayInputStream(split2Bytes)
|
||||||
coEvery {
|
val suffixName1 = getRandomString()
|
||||||
restorePlugin.getApkInputStream(token, packageName, "_$split1sha256")
|
val suffixName2 = getRandomString()
|
||||||
} returns split1InputStream
|
every { crypto.getNameForApk(salt, packageName, split1Name) } returns suffixName1
|
||||||
coEvery {
|
coEvery { backupPlugin.getInputStream(token, suffixName1) } returns split1InputStream
|
||||||
restorePlugin.getApkInputStream(token, packageName, "_$split2sha256")
|
every { crypto.getNameForApk(salt, packageName, split2Name) } returns suffixName2
|
||||||
} returns split2InputStream
|
coEvery { backupPlugin.getInputStream(token, suffixName2) } returns split2InputStream
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
apkInstaller.install(match { it.size == 3 }, packageName, installerName, any())
|
apkInstaller.install(match { it.size == 3 }, packageName, installerName, any())
|
||||||
|
@ -326,16 +374,23 @@ internal class ApkRestoreTest : TransportTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
|
apkRestore.restore(backup).collectIndexed { i, value ->
|
||||||
assertQueuedProgressSuccessFinished(i, value)
|
assertQueuedProgressSuccessFinished(i, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun swapPackages(packageMetadataMap: PackageMetadataMap): RestorableBackup {
|
||||||
|
val metadata = metadata.copy(packageMetadataMap = packageMetadataMap)
|
||||||
|
return backup.copy(backupMetadata = metadata)
|
||||||
|
}
|
||||||
|
|
||||||
private fun cacheBaseApkAndGetInfo(tmpDir: Path) {
|
private fun cacheBaseApkAndGetInfo(tmpDir: Path) {
|
||||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
coEvery { restorePlugin.getApkInputStream(token, packageName, "") } returns apkInputStream
|
every { crypto.getNameForApk(salt, packageName, "") } returns name
|
||||||
|
coEvery { backupPlugin.getInputStream(token, name) } returns apkInputStream
|
||||||
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||||
every {
|
every {
|
||||||
|
@Suppress("UNRESOLVED_REFERENCE")
|
||||||
pm.loadItemIcon(
|
pm.loadItemIcon(
|
||||||
packageInfo.applicationInfo,
|
packageInfo.applicationInfo,
|
||||||
packageInfo.applicationInfo
|
packageInfo.applicationInfo
|
||||||
|
|
|
@ -36,9 +36,9 @@ import kotlin.random.Random
|
||||||
internal class ApkBackupTest : BackupTest() {
|
internal class ApkBackupTest : BackupTest() {
|
||||||
|
|
||||||
private val pm: PackageManager = mockk()
|
private val pm: PackageManager = mockk()
|
||||||
private val streamGetter: suspend (suffix: String) -> OutputStream = mockk()
|
private val streamGetter: suspend (name: String) -> OutputStream = mockk()
|
||||||
|
|
||||||
private val apkBackup = ApkBackup(pm, settingsManager, metadataManager)
|
private val apkBackup = ApkBackup(pm, crypto, settingsManager, metadataManager)
|
||||||
|
|
||||||
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
|
||||||
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
private val signatureHash = byteArrayOf(0x03, 0x02, 0x01)
|
||||||
|
@ -140,7 +140,9 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
)
|
)
|
||||||
|
|
||||||
expectChecks()
|
expectChecks()
|
||||||
coEvery { streamGetter.invoke("") } returns apkOutputStream
|
every { metadataManager.salt } returns salt
|
||||||
|
every { crypto.getNameForApk(salt, packageInfo.packageName) } returns name
|
||||||
|
coEvery { streamGetter.invoke(name) } returns apkOutputStream
|
||||||
every {
|
every {
|
||||||
pm.getInstallSourceInfo(packageInfo.packageName)
|
pm.getInstallSourceInfo(packageInfo.packageName)
|
||||||
} returns InstallSourceInfo(null, null, null, updatedMetadata.installer)
|
} returns InstallSourceInfo(null, null, null, updatedMetadata.installer)
|
||||||
|
@ -197,11 +199,21 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
||||||
signatures = packageMetadata.signatures
|
signatures = packageMetadata.signatures
|
||||||
)
|
)
|
||||||
|
val suffixName1 = getRandomString()
|
||||||
|
val suffixName2 = getRandomString()
|
||||||
|
|
||||||
expectChecks()
|
expectChecks()
|
||||||
coEvery { streamGetter.invoke("") } returns apkOutputStream
|
every { metadataManager.salt } returns salt
|
||||||
coEvery { streamGetter.invoke("_$split1Sha256") } returns split1OutputStream
|
every { crypto.getNameForApk(salt, packageInfo.packageName) } returns name
|
||||||
coEvery { streamGetter.invoke("_$split2Sha256") } returns split2OutputStream
|
every {
|
||||||
|
crypto.getNameForApk(salt, packageInfo.packageName, split1Name)
|
||||||
|
} returns suffixName1
|
||||||
|
every {
|
||||||
|
crypto.getNameForApk(salt, packageInfo.packageName, split2Name)
|
||||||
|
} returns suffixName2
|
||||||
|
coEvery { streamGetter.invoke(name) } returns apkOutputStream
|
||||||
|
coEvery { streamGetter.invoke(suffixName1) } returns split1OutputStream
|
||||||
|
coEvery { streamGetter.invoke(suffixName2) } returns split2OutputStream
|
||||||
|
|
||||||
every {
|
every {
|
||||||
pm.getInstallSourceInfo(packageInfo.packageName)
|
pm.getInstallSourceInfo(packageInfo.packageName)
|
||||||
|
|
Loading…
Reference in a new issue