Let ApkBackup and ApkRestore use the new storage plugin API

This commit is contained in:
Torsten Grote 2021-09-23 12:37:28 +02:00 committed by Chirayu Desai
parent 183e34afd2
commit 50066f0317
12 changed files with 335 additions and 61 deletions

View file

@ -44,6 +44,10 @@ internal interface Crypto {
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
/**

View file

@ -8,9 +8,15 @@ data class RestorableBackup(val backupMetadata: BackupMetadata) {
val name: String
get() = backupMetadata.deviceName
val version: Byte
get() = backupMetadata.version
val token: Long
get() = backupMetadata.token
val salt: String
get() = backupMetadata.salt
val time: Long
get() = backupMetadata.time

View file

@ -122,6 +122,7 @@ internal class RestoreViewModel(
@Throws(RemoteException::class)
private fun getOrStartSession(): IRestoreSession {
@Suppress("UNRESOLVED_REFERENCE")
val session = this.session
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
?: throw RemoteException("beginRestoreSessionForUser returned null")
@ -155,7 +156,7 @@ internal class RestoreViewModel(
private fun getInstallResult(backup: RestorableBackup): LiveData<InstallResult> {
@Suppress("EXPERIMENTAL_API_USAGE")
return apkRestore.restore(backup.token, backup.deviceName, backup.packageMetadataMap)
return apkRestore.restore(backup)
.onStart {
Log.d(TAG, "Start InstallResult Flow")
}.catch { e ->

View file

@ -5,13 +5,15 @@ import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.util.Log
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.ApkSplit
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.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
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.getSignatures
import com.stevesoltys.seedvault.transport.backup.isSystemApp
@ -26,16 +28,19 @@ private val TAG = ApkRestore::class.java.simpleName
internal class ApkRestore(
private val context: Context,
private val backupPlugin: BackupPlugin,
private val restorePlugin: RestorePlugin,
private val crypto: Crypto,
private val splitCompatChecker: ApkSplitCompatibilityChecker,
private val apkInstaller: ApkInstaller
) {
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
val packages = packageMetadataMap.filter { it.value.hasApk() }
val packages = backup.packageMetadataMap.filter { it.value.hasApk() }
val total = packages.size
var progress = 0
@ -55,7 +60,7 @@ internal class ApkRestore(
// re-install individual packages and emit updates
for ((packageName, metadata) in packages) {
try {
restore(this, token, deviceName, packageName, metadata, installResult)
restore(this, backup, packageName, metadata, installResult)
} catch (e: IOException) {
Log.e(TAG, "Error re-installing APK for $packageName.", e)
emit(installResult.fail(packageName))
@ -75,14 +80,13 @@ internal class ApkRestore(
@Throws(IOException::class, SecurityException::class)
private suspend fun restore(
collector: FlowCollector<InstallResult>,
token: Long,
deviceName: String,
backup: RestorableBackup,
packageName: String,
metadata: PackageMetadata,
installResult: MutableInstallResult
) {
// 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
if (metadata.sha256 != sha256) throw SecurityException(
@ -139,7 +143,7 @@ internal class ApkRestore(
// process further APK splits, if available
val cachedApks =
cacheSplitsIfNeeded(token, deviceName, packageName, cachedApk, metadata.splits)
cacheSplitsIfNeeded(backup, packageName, cachedApk, metadata.splits)
if (cachedApks == null) {
Log.w(TAG, "Not installing $packageName because of incompatible splits.")
collector.emit(installResult.fail(packageName))
@ -161,8 +165,7 @@ internal class ApkRestore(
*/
@Throws(IOException::class, SecurityException::class)
private suspend fun cacheSplitsIfNeeded(
token: Long,
deviceName: String,
backup: RestorableBackup,
packageName: String,
cachedApk: File,
splits: List<ApkSplit>?
@ -171,15 +174,16 @@ internal class ApkRestore(
val splitNames = splits?.map { it.name } ?: return listOf(cachedApk)
// 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
val cachedApks = ArrayList<File>(splits.size + 1).apply {
add(cachedApk) // don't forget the base APK
}
splits.forEach { apkSplit -> // cache and check all splits
val suffix = "_${apkSplit.sha256}"
val (file, sha256) = cacheApk(token, packageName, suffix)
val suffix = if (backup.version == 0.toByte()) "_${apkSplit.sha256}" else apkSplit.name
val salt = backup.salt
val (file, sha256) = cacheApk(backup.version, backup.token, salt, packageName, suffix)
// check APK split's SHA-256 hash
if (apkSplit.sha256 != sha256) throw SecurityException(
"$packageName:${apkSplit.name} has sha256 '$sha256'," +
@ -199,14 +203,22 @@ internal class ApkRestore(
@Throws(IOException::class)
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
private suspend fun cacheApk(
version: Byte,
token: Long,
salt: String,
packageName: String,
suffix: String = ""
): Pair<File, String> {
// create a cache file to write the APK into
val cachedApk = File.createTempFile(packageName + suffix, ".apk", context.cacheDir)
// 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())
return Pair(cachedApk, sha256)
}

View file

@ -7,5 +7,5 @@ val installModule = module {
factory { ApkInstaller(androidContext()) }
factory { DeviceInfo(androidContext()) }
factory { ApkSplitCompatibilityChecker(get()) }
factory { ApkRestore(androidContext(), get(), get(), get()) }
factory { ApkRestore(androidContext(), get(), get(), get(), get(), get()) }
}

View file

@ -8,6 +8,7 @@ import android.content.pm.SigningInfo
import android.util.Log
import android.util.PackageUtils.computeSha256DigestBytes
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.metadata.ApkSplit
import com.stevesoltys.seedvault.metadata.MetadataManager
@ -27,6 +28,7 @@ private val TAG = ApkBackup::class.java.simpleName
@Suppress("BlockingMethodInNonBlockingContext")
internal class ApkBackup(
private val pm: PackageManager,
private val crypto: Crypto,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager
) {
@ -44,7 +46,7 @@ internal class ApkBackup(
suspend fun backupApkIfNecessary(
packageInfo: PackageInfo,
packageState: PackageState,
streamGetter: suspend (suffix: String) -> OutputStream
streamGetter: suspend (name: String) -> OutputStream
): PackageMetadata? {
// do not back up @pm@
val packageName = packageInfo.packageName
@ -102,7 +104,8 @@ internal class ApkBackup(
// get an InputStream for the APK
val inputStream = getApkInputStream(packageInfo.applicationInfo.sourceDir)
// 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
val splits =
@ -148,7 +151,7 @@ internal class ApkBackup(
@Throws(IOException::class)
private suspend fun backupSplitApks(
packageInfo: PackageInfo,
streamGetter: suspend (suffix: String) -> OutputStream
streamGetter: suspend (name: String) -> OutputStream
): List<ApkSplit> {
check(packageInfo.splitNames != null)
val splitSourceDirs = packageInfo.applicationInfo.splitSourceDirs
@ -159,7 +162,12 @@ internal class ApkBackup(
}
val splits = ArrayList<ApkSplit>(packageInfo.splitNames.size)
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)
}
return splits
@ -167,9 +175,10 @@ internal class ApkBackup(
@Throws(IOException::class)
private suspend fun backupSplitApk(
name: String,
packageName: String,
splitName: String,
sourceDir: String,
streamGetter: suspend (suffix: String) -> OutputStream
streamGetter: suspend (name: String) -> OutputStream
): ApkSplit {
// 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
@ -185,14 +194,14 @@ internal class ApkBackup(
}
}
val sha256 = messageDigest.digest().encodeBase64()
val suffix = "_$sha256"
val name = crypto.getNameForApk(metadataManager.salt, packageName, splitName)
// copy the split APK to the storage stream
getApkInputStream(sourceDir).use { inputStream ->
streamGetter(suffix).use { outputStream ->
streamGetter(name).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
return ApkSplit(name, sha256)
return ApkSplit(splitName, sha256)
}
}

View file

@ -453,9 +453,8 @@ internal class BackupCoordinator(
): Boolean {
val packageName = packageInfo.packageName
return try {
apkBackup.backupApkIfNecessary(packageInfo, packageState) { suffix ->
apkBackup.backupApkIfNecessary(packageInfo, packageState) { name ->
val token = settingsManager.getToken() ?: throw IOException("no current token")
val name = "${packageInfo.packageName}$suffix.apk"
plugin.getOutputStream(token, name)
}?.let { packageMetadata ->
plugin.getMetadataOutputStream().use {

View file

@ -14,6 +14,7 @@ val backupModule = module {
single {
ApkBackup(
pm = androidContext().packageManager,
crypto = get(),
settingsManager = get(),
metadataManager = get()
)

View file

@ -13,6 +13,7 @@ interface RestorePlugin {
* Returns an [InputStream] for the given token, for reading an APK that is to be restored.
*/
@Throws(IOException::class)
@Deprecated("Use only for v0 restores")
suspend fun getApkInputStream(token: Long, packageName: String, suffix: String): InputStream
}

View file

@ -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))
}
}

View file

@ -14,12 +14,14 @@ 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.restore.RestorableBackup
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.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import io.mockk.coEvery
import io.mockk.every
@ -48,16 +50,23 @@ internal class ApkRestoreTest : TransportTest() {
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 apkRestore: ApkRestore =
ApkRestore(strictContext, restorePlugin, splitCompatChecker, apkInstaller)
private val apkRestore: ApkRestore = ApkRestore(
strictContext,
backupPlugin,
restorePlugin,
crypto,
splitCompatChecker,
apkInstaller
)
private val icon: Drawable = mockk()
private val deviceName = getRandomString()
private val deviceName = metadata.deviceName
private val packageName = packageInfo.packageName
private val packageMetadata = PackageMetadata(
time = Random.nextLong(),
@ -71,6 +80,8 @@ internal class ApkRestoreTest : TransportTest() {
private val apkInputStream = ByteArrayInputStream(apkBytes)
private val appName = getRandomString()
private val installerName = packageMetadata.installer
private val backup = RestorableBackup(metadata.copy(packageMetadataMap = packageMetadataMap))
private val suffixName = getRandomString()
init {
// 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 {
// change SHA256 signature to random
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())
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)
}
}
@ -97,10 +109,11 @@ internal class ApkRestoreTest : TransportTest() {
packageInfo.packageName = getRandomString()
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
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
apkRestore.restore(backup).collectIndexed { i, value ->
assertQueuedFailFinished(i, value)
}
}
@ -112,7 +125,7 @@ internal class ApkRestoreTest : TransportTest() {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
} throws SecurityException()
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
apkRestore.restore(backup).collectIndexed { i, value ->
assertQueuedProgressFailFinished(i, value)
}
}
@ -134,7 +147,43 @@ internal class ApkRestoreTest : TransportTest() {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
} 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)
}
}
@ -181,7 +230,7 @@ internal class ApkRestoreTest : TransportTest() {
}
}
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
apkRestore.restore(backup).collectIndexed { i, value ->
when (i) {
0 -> {
val result = value[packageName]
@ -231,7 +280,7 @@ internal class ApkRestoreTest : TransportTest() {
splitCompatChecker.isCompatible(deviceName, listOf(split1Name, split2Name))
} returns false
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
apkRestore.restore(backup).collectIndexed { i, value ->
assertQueuedProgressFailFinished(i, value)
}
}
@ -240,20 +289,20 @@ internal class ApkRestoreTest : TransportTest() {
fun `split signature mismatch causes FAILED state`(@TempDir tmpDir: Path) = runBlocking {
// add one APK split to metadata
val splitName = getRandomString()
val sha256 = getRandomBase64(23)
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
cacheBaseApkAndGetInfo(tmpDir)
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
coEvery {
restorePlugin.getApkInputStream(token, packageName, "_$sha256")
backupPlugin.getInputStream(token, suffixName)
} returns ByteArrayInputStream(getRandomByteArray())
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
apkRestore.restore(backup).collectIndexed { i, value ->
assertQueuedProgressFailFinished(i, value)
}
}
@ -272,11 +321,10 @@ internal class ApkRestoreTest : TransportTest() {
cacheBaseApkAndGetInfo(tmpDir)
every { splitCompatChecker.isCompatible(deviceName, listOf(splitName)) } returns true
coEvery {
restorePlugin.getApkInputStream(token, packageName, "_$sha256")
} throws IOException()
every { crypto.getNameForApk(salt, packageName, splitName) } returns suffixName
coEvery { backupPlugin.getInputStream(token, suffixName) } throws IOException()
apkRestore.restore(token, deviceName, packageMetadataMap).collectIndexed { i, value ->
apkRestore.restore(backup).collectIndexed { i, value ->
assertQueuedProgressFailFinished(i, value)
}
}
@ -307,12 +355,12 @@ internal class ApkRestoreTest : TransportTest() {
val split2Bytes = byteArrayOf(0x07, 0x08, 0x09)
val split1InputStream = ByteArrayInputStream(split1Bytes)
val split2InputStream = ByteArrayInputStream(split2Bytes)
coEvery {
restorePlugin.getApkInputStream(token, packageName, "_$split1sha256")
} returns split1InputStream
coEvery {
restorePlugin.getApkInputStream(token, packageName, "_$split2sha256")
} returns split2InputStream
val suffixName1 = getRandomString()
val suffixName2 = getRandomString()
every { crypto.getNameForApk(salt, packageName, split1Name) } returns suffixName1
coEvery { backupPlugin.getInputStream(token, suffixName1) } returns split1InputStream
every { crypto.getNameForApk(salt, packageName, split2Name) } returns suffixName2
coEvery { backupPlugin.getInputStream(token, suffixName2) } returns split2InputStream
coEvery {
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)
}
}
private fun swapPackages(packageMetadataMap: PackageMetadataMap): RestorableBackup {
val metadata = metadata.copy(packageMetadataMap = packageMetadataMap)
return backup.copy(backupMetadata = metadata)
}
private fun cacheBaseApkAndGetInfo(tmpDir: Path) {
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 {
@Suppress("UNRESOLVED_REFERENCE")
pm.loadItemIcon(
packageInfo.applicationInfo,
packageInfo.applicationInfo

View file

@ -36,9 +36,9 @@ import kotlin.random.Random
internal class ApkBackupTest : BackupTest() {
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 signatureHash = byteArrayOf(0x03, 0x02, 0x01)
@ -140,7 +140,9 @@ internal class ApkBackupTest : BackupTest() {
)
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 {
pm.getInstallSourceInfo(packageInfo.packageName)
} returns InstallSourceInfo(null, null, null, updatedMetadata.installer)
@ -197,11 +199,21 @@ internal class ApkBackupTest : BackupTest() {
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
signatures = packageMetadata.signatures
)
val suffixName1 = getRandomString()
val suffixName2 = getRandomString()
expectChecks()
coEvery { streamGetter.invoke("") } returns apkOutputStream
coEvery { streamGetter.invoke("_$split1Sha256") } returns split1OutputStream
coEvery { streamGetter.invoke("_$split2Sha256") } returns split2OutputStream
every { metadataManager.salt } returns salt
every { crypto.getNameForApk(salt, packageInfo.packageName) } returns name
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 {
pm.getInstallSourceInfo(packageInfo.packageName)