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)