Let ApkBackup and ApkRestore use the new storage plugin API
diff --git a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt
index fb0d509..4d6cf6c 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/crypto/Crypto.kt
@@ -44,6 +44,10 @@
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
/**
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
index ddcaeb8..f1c33c1 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestorableBackup.kt
@@ -8,9 +8,15 @@
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
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
index 99f8c20..80a23e0 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt
@@ -122,6 +122,7 @@
@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 @@
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 ->
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt
index d321532..dcc4165 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/ApkRestore.kt
@@ -5,13 +5,15 @@
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 @@
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 @@
// 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 @@
@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 @@
// 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 @@
*/
@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 @@
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 @@
@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)
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt
index 60cc448..33e640b 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/restore/install/InstallModule.kt
@@ -7,5 +7,5 @@
factory { ApkInstaller(androidContext()) }
factory { DeviceInfo(androidContext()) }
factory { ApkSplitCompatibilityChecker(get()) }
- factory { ApkRestore(androidContext(), get(), get(), get()) }
+ factory { ApkRestore(androidContext(), get(), get(), get(), get(), get()) }
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
index b38fbc7..7f27e40 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/ApkBackup.kt
@@ -8,6 +8,7 @@
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 @@
@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 @@
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 @@
// 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 @@
@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 @@
}
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 @@
@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 @@
}
}
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)
}
}
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
index ef12f76..7e19d9d 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt
@@ -453,9 +453,8 @@
): 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 {
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
index 5e119b4..8be00ac 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupModule.kt
@@ -14,6 +14,7 @@
single {
ApkBackup(
pm = androidContext().packageManager,
+ crypto = get(),
settingsManager = get(),
metadataManager = get()
)
diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
index a0e23b2..6fa75cd 100644
--- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
+++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestorePlugin.kt
@@ -13,6 +13,7 @@
* 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
}
diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt
new file mode 100644
index 0000000..981ace2
--- /dev/null
+++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkBackupRestoreTest.kt
@@ -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))
+ }
+
+}
diff --git a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt
index da50652..6f61608 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/restore/install/ApkRestoreTest.kt
@@ -14,12 +14,14 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 @@
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 @@
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 @@
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 @@
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 @@
)
}
- 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
diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
index 626c11f..2137e56 100644
--- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
+++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/ApkBackupTest.kt
@@ -36,9 +36,9 @@
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 @@
)
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 @@
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)