Back up APKs to storage (when they changed) and save metadata about them

This commit is contained in:
Torsten Grote 2019-12-19 17:09:52 -03:00
parent b9cac5ea87
commit 81c2031ce7
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
12 changed files with 418 additions and 23 deletions

View file

@ -52,6 +52,27 @@ class MetadataManager(
writeMetadataToCache()
}
/**
* Call this after an APK as been successfully written to backup storage.
* It will update the package's metadata, but NOT write it storage or internal cache.
* You still need to call [onPackageBackedUp] afterwards to write it out.
*/
@Synchronized
fun onApkBackedUp(packageName: String, packageMetadata: PackageMetadata) {
metadata.packageMetadata[packageName]?.let {
check(it.time <= packageMetadata.time) {
"APK backup set time of $packageName backwards"
}
check(packageMetadata.version != null) {
"APK backup returned version null"
}
check(it.version == null || it.version < packageMetadata.version) {
"APK backup backed up the same or a smaller version: was ${it.version} is ${packageMetadata.version}"
}
}
metadata.packageMetadata[packageName] = packageMetadata
}
/**
* Call this after a package has been backed up successfully.
*
@ -91,6 +112,11 @@ class MetadataManager(
@Synchronized
fun getLastBackupTime(): Long = metadata.time
@Synchronized
fun getPackageMetadata(packageName: String): PackageMetadata? {
return metadata.packageMetadata[packageName]?.copy()
}
@Synchronized
@VisibleForTesting
private fun getMetadataFromCache(): BackupMetadata? {

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.plugins.saf
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.stevesoltys.seedvault.transport.backup.BackupPlugin
import com.stevesoltys.seedvault.transport.backup.FullBackupPlugin
@ -7,6 +8,8 @@ import com.stevesoltys.seedvault.transport.backup.KVBackupPlugin
import java.io.IOException
import java.io.OutputStream
private const val MIME_TYPE_APK = "application/vnd.android.package-archive"
internal class DocumentsProviderBackupPlugin(
private val storage: DocumentsStorage,
packageManager: PackageManager) : BackupPlugin {
@ -41,6 +44,13 @@ internal class DocumentsProviderBackupPlugin(
return storage.getOutputStream(metadataFile)
}
@Throws(IOException::class)
override fun getApkOutputStream(packageInfo: PackageInfo): OutputStream {
val setDir = storage.getSetDir() ?: throw IOException()
val file = setDir.createOrGetFile("${packageInfo.packageName}.apk", MIME_TYPE_APK)
return storage.getOutputStream(file)
}
override val providerPackageName: String? by lazy {
val authority = storage.getAuthority() ?: return@lazy null
val providerInfo = packageManager.resolveContentProvider(authority, 0) ?: return@lazy null

View file

@ -62,6 +62,11 @@ class SettingsManager(context: Context) {
return FlashDrive(name, serialNumber, vendorId, productId)
}
fun backupApks(): Boolean {
// TODO
return true
}
}
data class Storage(

View file

@ -0,0 +1,141 @@
package com.stevesoltys.seedvault.transport.backup
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.content.pm.SigningInfo
import android.util.Log
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.encodeBase64
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.settings.SettingsManager
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.OutputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
private val TAG = ApkBackup::class.java.simpleName
class ApkBackup(
private val pm: PackageManager,
private val clock: Clock,
private val settingsManager: SettingsManager,
private val metadataManager: MetadataManager) {
@Throws(IOException::class)
fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): Boolean {
// do not back up @pm@
val packageName = packageInfo.packageName
if (packageName == MAGIC_PACKAGE_MANAGER) return false
// do not back up when setting is not enabled
if (!settingsManager.backupApks()) return false
// do not back up system apps that haven't been updated
val isSystemApp = packageInfo.applicationInfo.flags and FLAG_SYSTEM != 0
val isUpdatedSystemApp = packageInfo.applicationInfo.flags and FLAG_UPDATED_SYSTEM_APP != 0
if (isSystemApp && !isUpdatedSystemApp) {
Log.d(TAG, "Package $packageName is vanilla system app. Not backing it up.")
return false
}
// get cached metadata about package
val packageMetadata = metadataManager.getPackageMetadata(packageName)
?: PackageMetadata(time = clock.time())
// TODO remove when adding support in [signaturesChanged]
if (packageInfo.signingInfo.hasMultipleSigners()) {
Log.e(TAG, "Package $packageName has multiple signers. Not backing it up.")
return false
}
// get signatures
val signatures = getSignatures(packageInfo.signingInfo)
if (signatures.isEmpty()) {
Log.e(TAG, "Package $packageName has no signatures. Not backing it up.")
return false
}
// get version codes
val version = packageInfo.longVersionCode
val backedUpVersion = packageMetadata.version ?: 0L // no version will cause backup
// do not backup if we have the version already and signatures did not change
if (version <= backedUpVersion && !signaturesChanged(packageMetadata, signatures)) {
Log.d(TAG, "Package $packageName with version $version already has a backup ($backedUpVersion) with the same signature. Not backing it up.")
return false
}
// get an InputStream for the APK
val apk = File(packageInfo.applicationInfo.sourceDir)
val inputStream = try {
apk.inputStream()
} catch (e: FileNotFoundException) {
Log.e(TAG, "Error opening ${apk.absolutePath} for backup.", e)
throw IOException(e)
} catch (e: SecurityException) {
Log.e(TAG, "Error opening ${apk.absolutePath} for backup.", e)
throw IOException(e)
}
// copy the APK to the storage's output
streamGetter.invoke().use { outputStream ->
inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
Log.d(TAG, "Backed up new APK of $packageName with version $version.")
// update the metadata
val installer = pm.getInstallerPackageName(packageName)
val updatedMetadata = PackageMetadata(
time = clock.time(),
version = version,
installer = installer,
signatures = signatures
)
metadataManager.onApkBackedUp(packageName, updatedMetadata)
return true
}
private fun getSignatures(signingInfo: SigningInfo): List<String> {
val signatures = ArrayList<String>()
if (signingInfo.hasMultipleSigners()) {
for (sig in signingInfo.apkContentsSigners) {
signatures.add(hashSignature(sig).encodeBase64())
}
} else {
for (sig in signingInfo.signingCertificateHistory) {
signatures.add(hashSignature(sig).encodeBase64())
}
}
return signatures
}
private fun hashSignature(signature: Signature): ByteArray {
try {
val digest = MessageDigest.getInstance("SHA-256")
digest.update(signature.toByteArray())
return digest.digest()
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, "No SHA-256 algorithm found!", e)
throw AssertionError(e)
}
}
private fun signaturesChanged(packageMetadata: PackageMetadata, signatures: List<String>): Boolean {
// no signatures in package metadata counts as them not having changed
if (packageMetadata.signatures == null) return false
// TODO this is probably more complicated, need to verify
// 1. multiple signers: need to match all signatures in list
// 2. single signer (with or without history): the intersection of both lists must not be empty.
return packageMetadata.signatures.intersect(signatures).isEmpty()
}
}

View file

@ -23,6 +23,7 @@ internal class BackupCoordinator(
private val plugin: BackupPlugin,
private val kv: KVBackup,
private val full: FullBackup,
private val apkBackup: ApkBackup,
private val metadataManager: MetadataManager,
private val settingsManager: SettingsManager,
private val nm: BackupNotificationManager) {
@ -196,6 +197,7 @@ internal class BackupCoordinator(
if (result != TRANSPORT_OK) return result
val packageName = packageInfo.packageName
try {
apkBackup.backupApkIfNecessary(packageInfo) { plugin.getApkOutputStream(packageInfo) }
val outputStream = plugin.getMetadataOutputStream()
metadataManager.onPackageBackedUp(packageName, outputStream)
} catch (e: IOException) {

View file

@ -5,7 +5,8 @@ import org.koin.dsl.module
val backupModule = module {
single { InputFactory() }
single { ApkBackup(androidContext().packageManager, get(), get(), get()) }
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
single { FullBackup(get<BackupPlugin>().fullBackupPlugin, get(), get(), get()) }
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get()) }
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get()) }
}

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.transport.backup
import android.content.pm.PackageInfo
import java.io.IOException
import java.io.OutputStream
@ -21,6 +22,12 @@ interface BackupPlugin {
@Throws(IOException::class)
fun getMetadataOutputStream(): OutputStream
/**
* Returns an [OutputStream] for writing an APK to be backed up.
*/
@Throws(IOException::class)
fun getApkOutputStream(packageInfo: PackageInfo): OutputStream
/**
* Returns the package name of the app that provides the backend storage
* which is used for the current backup location.

View file

@ -17,6 +17,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.koin.core.context.stopKoin
import java.io.*
import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
class MetadataManagerTest {
@ -29,6 +30,7 @@ class MetadataManagerTest {
private val manager = MetadataManager(context, clock, metadataWriter, metadataReader)
private val time = 42L
private val packageName = getRandomString()
private val initialMetadata = BackupMetadata(token = time)
private val storageOutputStream = ByteArrayOutputStream()
private val cacheOutputStream: FileOutputStream = mockk()
@ -52,9 +54,49 @@ class MetadataManagerTest {
assertEquals(0L, manager.getLastBackupTime())
}
@Test
fun `test onApkBackedUp() with no prior package metadata`() {
val packageMetadata = PackageMetadata(
time = time + 1,
version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(),
signatures = listOf("sig")
)
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
every { clock.time() } returns time
manager.onApkBackedUp(packageName, packageMetadata)
assertEquals(packageMetadata, manager.getPackageMetadata(packageName))
}
@Test
fun `test onApkBackedUp() with existing package metadata`() {
val cachedMetadata = initialMetadata.copy()
val packageMetadata = PackageMetadata(
time = time,
version = Random.nextLong(Long.MAX_VALUE),
installer = getRandomString(),
signatures = listOf("sig")
)
cachedMetadata.packageMetadata[packageName] = packageMetadata
val updatedPackageMetadata = PackageMetadata(
time = time + 1,
version = packageMetadata.version!! + 1,
installer = getRandomString(),
signatures = listOf("sig foo")
)
expectReadFromCache(cachedMetadata)
manager.onApkBackedUp(packageName, updatedPackageMetadata)
assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName))
}
@Test
fun `test onPackageBackedUp()`() {
val packageName = getRandomString()
val updatedMetadata = initialMetadata.copy()
updatedMetadata.time = time
updatedMetadata.packageMetadata[packageName] = PackageMetadata(time)
@ -71,13 +113,13 @@ class MetadataManagerTest {
@Test
fun `test onPackageBackedUp() fails to write to storage`() {
val packageName = getRandomString()
val updateTime = time + 1
val updatedMetadata = initialMetadata.copy()
updatedMetadata.time = time
updatedMetadata.packageMetadata[packageName] = PackageMetadata(time)
updatedMetadata.time = updateTime
updatedMetadata.packageMetadata[packageName] = PackageMetadata(updateTime)
every { context.openFileInput(METADATA_CACHE_FILE) } throws FileNotFoundException()
every { clock.time() } returns time
every { clock.time() } returns time andThen updateTime
every { metadataWriter.write(updatedMetadata, storageOutputStream) } throws IOException()
try {
@ -88,31 +130,32 @@ class MetadataManagerTest {
}
assertEquals(0L, manager.getLastBackupTime()) // time was reverted
// TODO also assert reverted PackageMetadata once possible
assertEquals(initialMetadata.packageMetadata[packageName], manager.getPackageMetadata(packageName))
}
@Test
fun `test onPackageBackedUp() with filled cache`() {
val cachedPackageName = getRandomString()
val packageName = getRandomString()
val byteArray = ByteArray(DEFAULT_BUFFER_SIZE)
val cachedMetadata = initialMetadata.copy(time = 23)
cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(23)
cachedMetadata.packageMetadata[packageName] = PackageMetadata(23)
val cacheTime = time - 1
val cachedMetadata = initialMetadata.copy(time = cacheTime)
cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(cacheTime)
cachedMetadata.packageMetadata[packageName] = PackageMetadata(cacheTime)
every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream
every { cacheInputStream.available() } returns byteArray.size andThen 0
every { cacheInputStream.read(byteArray) } returns -1
every { metadataReader.decode(ByteArray(0)) } returns cachedMetadata
val updatedMetadata = cachedMetadata.copy(time = time)
cachedMetadata.packageMetadata[cachedPackageName] = PackageMetadata(time)
cachedMetadata.packageMetadata[packageName] = PackageMetadata(time)
expectReadFromCache(cachedMetadata)
every { clock.time() } returns time
every { metadataWriter.write(cachedMetadata, storageOutputStream) } just Runs
expectWriteToCache(cachedMetadata)
every { metadataWriter.write(updatedMetadata, storageOutputStream) } just Runs
expectWriteToCache(updatedMetadata)
manager.onPackageBackedUp(packageName, storageOutputStream)
assertEquals(time, manager.getLastBackupTime())
// TODO also assert updated PackageMetadata once possible
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
assertEquals(PackageMetadata(time), manager.getPackageMetadata(packageName))
}
private fun expectWriteToCache(metadata: BackupMetadata) {
@ -121,4 +164,12 @@ class MetadataManagerTest {
every { cacheOutputStream.write(encodedMetadata) } just Runs
}
private fun expectReadFromCache(metadata: BackupMetadata) {
val byteArray = ByteArray(DEFAULT_BUFFER_SIZE)
every { context.openFileInput(METADATA_CACHE_FILE) } returns cacheInputStream
every { cacheInputStream.available() } returns byteArray.size andThen 0
every { cacheInputStream.read(byteArray) } returns -1
every { metadataReader.decode(ByteArray(0)) } returns metadata
}
}

View file

@ -23,6 +23,7 @@ import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.OutputStream
import kotlin.random.Random
internal class CoordinatorIntegrationTest : TransportTest() {
@ -41,8 +42,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val kvBackup = KVBackup(kvBackupPlugin, inputFactory, headerWriter, cryptoImpl)
private val fullBackupPlugin = mockk<FullBackupPlugin>()
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
private val apkBackup = mockk<ApkBackup>()
private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, metadataManager, settingsManager, notificationManager)
private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, metadataManager, settingsManager, notificationManager)
private val restorePlugin = mockk<RestorePlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>()
@ -91,6 +93,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData2.size
}
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns true
every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
@ -143,7 +146,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
appData.size
}
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream
every { settingsManager.saveNewBackupTime() } just Runs
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns false
every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
// start and finish K/V backup
assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
@ -179,6 +184,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns true
every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs

View file

@ -1,8 +1,13 @@
package com.stevesoltys.seedvault.transport
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
import android.content.pm.ApplicationInfo.FLAG_INSTALLED
import android.content.pm.PackageInfo
import android.content.pm.SigningInfo
import android.util.Log
import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.settings.SettingsManager
@ -11,16 +16,26 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD
import kotlin.random.Random
@TestInstance(PER_METHOD)
abstract class TransportTest {
protected val clock: Clock = mockk()
protected val crypto = mockk<Crypto>()
protected val settingsManager = mockk<SettingsManager>()
protected val metadataManager = mockk<MetadataManager>()
protected val context = mockk<Context>(relaxed = true)
protected val packageInfo = PackageInfo().apply { packageName = "org.example" }
protected val sigInfo: SigningInfo = mockk()
protected val packageInfo = PackageInfo().apply {
packageName = "org.example"
longVersionCode = Random.nextLong()
applicationInfo = ApplicationInfo().apply {
flags = FLAG_ALLOW_BACKUP or FLAG_INSTALLED
}
signingInfo = sigInfo
}
init {
mockkStatic(Log::class)

View file

@ -0,0 +1,130 @@
package com.stevesoltys.seedvault.transport.backup
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.getRandomByteArray
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageMetadata
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.io.OutputStream
import java.nio.file.Path
import kotlin.random.Random
internal class ApkBackupTest : BackupTest() {
private val pm: PackageManager = mockk()
private val streamGetter: () -> OutputStream = mockk()
private val apkBackup = ApkBackup(pm, clock, settingsManager, metadataManager)
private val signatureBytes = byteArrayOf(0x01, 0x02, 0x03)
private val sigs = arrayOf(Signature(signatureBytes))
private val packageMetadata = PackageMetadata(
time = Random.nextLong(),
version = packageInfo.longVersionCode - 1,
signatures = listOf("A5BYxvLAy0ksUzsKTRTvd8wPeKvMztUofYShogEc-4E")
)
@Test
fun `does not back up @pm@`() {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
}
@Test
fun `does not back up when setting disabled`() {
every { settingsManager.backupApks() } returns false
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
}
@Test
fun `does not back up system apps`() {
packageInfo.applicationInfo.flags = FLAG_SYSTEM
every { settingsManager.backupApks() } returns true
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
}
@Test
fun `does not back up the same version`() {
packageInfo.applicationInfo.flags = FLAG_UPDATED_SYSTEM_APP
val packageMetadata = packageMetadata.copy(
version = packageInfo.longVersionCode
)
expectChecks(packageMetadata)
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
}
@Test
fun `does back up the same version when signatures changes`() {
packageInfo.applicationInfo.sourceDir = "/tmp/doesNotExist"
expectChecks()
assertThrows(IOException::class.java) {
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
}
}
@Test
fun `do not accept empty signature`() {
every { settingsManager.backupApks() } returns true
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns packageMetadata
every { sigInfo.hasMultipleSigners() } returns false
every { sigInfo.signingCertificateHistory } returns emptyArray()
assertFalse(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
}
@Test
fun `test successful APK backup`(@TempDir tmpDir: Path) {
val apkBytes = getRandomByteArray()
val tmpFile = File(tmpDir.toAbsolutePath().toString())
packageInfo.applicationInfo.sourceDir = File(tmpFile, "test.apk").apply {
assertTrue(createNewFile())
writeBytes(apkBytes)
}.absolutePath
val apkOutputStream = ByteArrayOutputStream()
val updatedMetadata = PackageMetadata(
time = Random.nextLong(),
version = packageInfo.longVersionCode,
installer = getRandomString(),
signatures = packageMetadata.signatures
)
expectChecks()
every { streamGetter.invoke() } returns apkOutputStream
every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer
every { clock.time() } returns updatedMetadata.time
every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata) } just Runs
assertTrue(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
assertArrayEquals(apkBytes, apkOutputStream.toByteArray())
}
private fun expectChecks(packageMetadata: PackageMetadata = this.packageMetadata) {
every { settingsManager.backupApks() } returns true
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns packageMetadata
every { sigInfo.hasMultipleSigners() } returns false
every { sigInfo.signingCertificateHistory } returns sigs
}
}

View file

@ -23,9 +23,10 @@ internal class BackupCoordinatorTest: BackupTest() {
private val plugin = mockk<BackupPlugin>()
private val kv = mockk<KVBackup>()
private val full = mockk<FullBackup>()
private val apkBackup = mockk<ApkBackup>()
private val notificationManager = mockk<BackupNotificationManager>()
private val backup = BackupCoordinator(context, plugin, kv, full, metadataManager, settingsManager, notificationManager)
private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, metadataManager, settingsManager, notificationManager)
private val metadataOutputStream = mockk<OutputStream>()