diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt index b05dec43..b3f04ec3 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/Metadata.kt @@ -39,6 +39,11 @@ enum class PackageState { * Package data could not get backed up, because the app reported no data to back up. */ NO_DATA, + /** + * Package data could not get backed up, because it was not allowed. + * Most often, this is a manifest opt-out, but it could also be a disabled or system-user app. + */ + NOT_ALLOWED, /** * Package data could not get backed up, because an error occurred during backup. */ diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt index 98c5e4ea..20bd3caf 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA +import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream @@ -67,10 +68,16 @@ class MetadataManager( "APK backup backed up the same or a smaller version: was ${it.version} is ${packageMetadata.version}" } } + val oldPackageMetadata = metadata.packageMetadataMap[packageName] + ?: PackageMetadata() + // only allow state change if backup of this package is not allowed + val newState = if (packageMetadata.state == NOT_ALLOWED) + packageMetadata.state + else + oldPackageMetadata.state modifyMetadata(metadataOutputStream) { - val oldPackageMetadata = metadata.packageMetadataMap[packageName] - ?: PackageMetadata() metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy( + state = newState, version = packageMetadata.version, installer = packageMetadata.installer, sha256 = packageMetadata.sha256, diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt index 719a3648..e6713b6a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataReader.kt @@ -64,6 +64,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader { "" -> APK_AND_DATA QUOTA_EXCEEDED.name -> QUOTA_EXCEEDED NO_DATA.name -> NO_DATA + NOT_ALLOWED.name -> NOT_ALLOWED else -> UNKNOWN_ERROR } val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index 43a34ed1..f4392134 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.NotificationBackupObserver import com.stevesoltys.seedvault.R +import com.stevesoltys.seedvault.transport.backup.PackageService import org.koin.core.context.GlobalContext.get private val TAG = ConfigurableBackupTransportService::class.java.simpleName @@ -55,9 +56,10 @@ fun requestBackup(context: Context) { val nm: BackupNotificationManager = get().koin.get() nm.onBackupUpdate(context.getString(R.string.notification_backup_starting), 0, 1, true) + val packageService: PackageService = get().koin.get() val observer = NotificationBackupObserver(context, true) val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED - val packages = PackageService.eligiblePackages + val packages = packageService.eligiblePackages val result = try { val backupManager: IBackupManager = get().koin.get() backupManager.requestBackup(packages, observer, BackupMonitor(), flags) 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 cbbd6c53..1a69b601 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 @@ -12,6 +12,7 @@ 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.metadata.PackageState import com.stevesoltys.seedvault.settings.SettingsManager import java.io.File import java.io.FileNotFoundException @@ -35,7 +36,7 @@ class ApkBackup( * @return new [PackageMetadata] if an APK backup was made or null if no backup was made. */ @Throws(IOException::class) - fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): PackageMetadata? { + fun backupApkIfNecessary(packageInfo: PackageInfo, packageState: PackageState, streamGetter: () -> OutputStream): PackageMetadata? { // do not back up @pm@ val packageName = packageInfo.packageName if (packageName == MAGIC_PACKAGE_MANAGER) return null @@ -108,6 +109,7 @@ class ApkBackup( // return updated metadata return PackageMetadata( + state = packageState, version = version, installer = pm.getInstallerPackageName(packageName), sha256 = 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 d5aa8d28..921f52cf 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 @@ -28,6 +28,7 @@ internal class BackupCoordinator( private val full: FullBackup, private val apkBackup: ApkBackup, private val clock: Clock, + private val packageService: PackageService, private val metadataManager: MetadataManager, private val settingsManager: SettingsManager, private val nm: BackupNotificationManager) { @@ -115,10 +116,14 @@ internal class BackupCoordinator( fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int { cancelReason = UNKNOWN_ERROR val packageName = packageInfo.packageName - // backups of package manager metadata do not respect backoff - // we need to reject them manually when now is not a good time for a backup - if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) { - return TRANSPORT_PACKAGE_REJECTED + if (packageName == MAGIC_PACKAGE_MANAGER) { + // backups of package manager metadata do not respect backoff + // we need to reject them manually when now is not a good time for a backup + if (getBackupBackoff() != 0L) { + return TRANSPORT_PACKAGE_REJECTED + } + // hook in here to back up APKs of apps that are otherwise not allowed for backup + backUpNotAllowedPackages() } val result = kv.performBackup(packageInfo, data, flags) return backUpApk(result, packageInfo) @@ -238,10 +243,22 @@ internal class BackupCoordinator( else -> throw IllegalStateException("Unexpected state in finishBackup()") } - private fun backUpApk(result: Int, packageInfo: PackageInfo): Int { + private fun backUpNotAllowedPackages() { + Log.d(TAG, "Checking if APKs of opt-out apps need backup...") + packageService.notAllowedPackages.forEach { optOutPackageInfo -> + try { + backUpApk(0, optOutPackageInfo, NOT_ALLOWED) + } catch (e: IOException) { + Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e) + } + } + } + + private fun backUpApk(result: Int, packageInfo: PackageInfo, packageState: PackageState = UNKNOWN_ERROR): Int { val packageName = packageInfo.packageName + if (packageName == MAGIC_PACKAGE_MANAGER) return result return try { - apkBackup.backupApkIfNecessary(packageInfo) { + apkBackup.backupApkIfNecessary(packageInfo, packageState) { plugin.getApkOutputStream(packageInfo) }?.let { packageMetadata -> val outputStream = plugin.getMetadataOutputStream() 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 d76dd68a..8ed93990 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 @@ -5,8 +5,9 @@ import org.koin.dsl.module val backupModule = module { single { InputFactory() } + single { PackageService(androidContext().packageManager, get()) } single { ApkBackup(androidContext().packageManager, get(), get()) } single { KVBackup(get().kvBackupPlugin, get(), get(), get()) } single { FullBackup(get().fullBackupPlugin, get(), get(), get()) } - single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get()) } + single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt similarity index 61% rename from app/src/main/java/com/stevesoltys/seedvault/transport/PackageService.kt rename to app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt index f1d699de..0710c70a 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/PackageService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt @@ -1,17 +1,18 @@ -package com.stevesoltys.seedvault.transport +package com.stevesoltys.seedvault.transport.backup import android.app.backup.IBackupManager import android.content.pm.IPackageManager import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.os.RemoteException import android.os.ServiceManager.getService import android.os.UserHandle import android.util.Log import android.util.Log.INFO +import androidx.annotation.WorkerThread import com.google.android.collect.Sets.newArraySet import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER -import org.koin.core.KoinComponent -import org.koin.core.inject private val TAG = PackageService::class.java.simpleName @@ -30,15 +31,19 @@ private val IGNORED_PACKAGES = newArraySet( * @author Steve Soltys * @author Torsten Grote */ -internal object PackageService : KoinComponent { +internal class PackageService( + private val packageManager: PackageManager, + private val backupManager: IBackupManager) { - private val backupManager: IBackupManager by inject() - private val packageManager: IPackageManager = IPackageManager.Stub.asInterface(getService("package")) + // TODO This can probably be removed and PackageManager#getInstalledPackages() used instead + private val packageManagerService: IPackageManager = IPackageManager.Stub.asInterface(getService("package")) + private val myUserId = UserHandle.myUserId() val eligiblePackages: Array + @WorkerThread @Throws(RemoteException::class) get() { - val packages: List = packageManager.getInstalledPackages(0, UserHandle.USER_SYSTEM).list as List + val packages: List = packageManagerService.getInstalledPackages(0, UserHandle.USER_SYSTEM).list as List val packageList = packages .map { packageInfo -> packageInfo.packageName } .filter { packageName -> !IGNORED_PACKAGES.contains(packageName) } @@ -53,7 +58,7 @@ internal object PackageService : KoinComponent { } // TODO why is this filtering out so much? - val eligibleApps = backupManager.filterAppsEligibleForBackupForUser(UserHandle.myUserId(), packageList.toTypedArray()) + val eligibleApps = backupManager.filterAppsEligibleForBackupForUser(myUserId, packageList.toTypedArray()) // log eligible packages if (Log.isLoggable(TAG, INFO)) { @@ -70,4 +75,19 @@ internal object PackageService : KoinComponent { return packageArray.toTypedArray() } + val notAllowedPackages: List + @WorkerThread + get() { + val installed = packageManager.getInstalledPackages(GET_SIGNING_CERTIFICATES) + val installedArray = installed.map { packageInfo -> + packageInfo.packageName + }.toTypedArray() + + val eligible = backupManager.filterAppsEligibleForBackupForUser(myUserId, installedArray) + + return installed.filter { packageInfo -> + packageInfo.packageName !in eligible + } + } + } diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt index a18843d0..7f27bff9 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataManagerTest.kt @@ -6,7 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.getRandomByteArray import com.stevesoltys.seedvault.getRandomString -import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA +import com.stevesoltys.seedvault.metadata.PackageState.* import io.mockk.Runs import io.mockk.every import io.mockk.just @@ -97,6 +97,40 @@ class MetadataManagerTest { assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName)) } + @Test + fun `test onApkBackedUp() limits state changes`() { + var version = Random.nextLong(Long.MAX_VALUE) + var packageMetadata = PackageMetadata( + version = version, + installer = getRandomString(), + signatures = listOf("sig") + ) + + expectReadFromCache() + expectModifyMetadata(initialMetadata) + val oldState = UNKNOWN_ERROR + + // state doesn't change for APK_AND_DATA + packageMetadata = packageMetadata.copy(version = ++version, state = APK_AND_DATA) + manager.onApkBackedUp(packageName, packageMetadata, storageOutputStream) + assertEquals(packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName)) + + // state doesn't change for QUOTA_EXCEEDED + packageMetadata = packageMetadata.copy(version = ++version, state = QUOTA_EXCEEDED) + manager.onApkBackedUp(packageName, packageMetadata, storageOutputStream) + assertEquals(packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName)) + + // state doesn't change for NO_DATA + packageMetadata = packageMetadata.copy(version = ++version, state = NO_DATA) + manager.onApkBackedUp(packageName, packageMetadata, storageOutputStream) + assertEquals(packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName)) + + // state DOES change for NOT_ALLOWED + packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED) + manager.onApkBackedUp(packageName, packageMetadata, storageOutputStream) + assertEquals(packageMetadata.copy(state = NOT_ALLOWED), manager.getPackageMetadata(packageName)) + } + @Test fun `test onPackageBackedUp()`() { val updatedMetadata = initialMetadata.copy() diff --git a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt index dfdeefc7..c7139085 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/metadata/MetadataWriterDecoderTest.kt @@ -50,7 +50,7 @@ internal class MetadataWriterDecoderTest { } @Test - fun `encoded metadata matches decoded metadata (two full packages)`() { + fun `encoded metadata matches decoded metadata (three full packages)`() { val packages = HashMap().apply { put(getRandomString(), PackageMetadata( time = Random.nextLong(), @@ -68,6 +68,14 @@ internal class MetadataWriterDecoderTest { sha256 = getRandomString(), signatures = listOf(getRandomString(), getRandomString()) )) + put(getRandomString(), PackageMetadata( + time = 0L, + state = NOT_ALLOWED, + version = Random.nextLong(), + installer = getRandomString(), + sha256 = getRandomString(), + signatures = listOf(getRandomString(), getRandomString()) + )) } val metadata = getMetadata(packages) assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)) diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 0780d02d..3c38a3ae 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.header.HeaderWriterImpl import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH import com.stevesoltys.seedvault.metadata.MetadataReaderImpl import com.stevesoltys.seedvault.metadata.PackageMetadata +import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.transport.backup.* import com.stevesoltys.seedvault.transport.restore.* import io.mockk.* @@ -43,8 +44,9 @@ internal class CoordinatorIntegrationTest : TransportTest() { private val fullBackupPlugin = mockk() private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl) private val apkBackup = mockk() + private val packageService:PackageService = mockk() private val notificationManager = mockk() - private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, clock, metadataManager, settingsManager, notificationManager) + private val backup = BackupCoordinator(context, backupPlugin, kvBackup, fullBackup, apkBackup, clock, packageService, metadataManager, settingsManager, notificationManager) private val restorePlugin = mockk() private val kvRestorePlugin = mockk() @@ -94,7 +96,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData2.size } every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2 - every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata + every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs @@ -148,7 +150,7 @@ internal class CoordinatorIntegrationTest : TransportTest() { appData.size } every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream - every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns null + every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs @@ -186,7 +188,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 packageMetadata + every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata every { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs 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 9cab41b1..1a2b4027 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 @@ -45,14 +45,14 @@ internal class ApkBackupTest : BackupTest() { @Test fun `does not back up @pm@`() { val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } - assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) } @Test fun `does not back up when setting disabled`() { every { settingsManager.backupApks() } returns false - assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) } @Test @@ -61,7 +61,7 @@ internal class ApkBackupTest : BackupTest() { every { settingsManager.backupApks() } returns true - assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) } @Test @@ -73,7 +73,7 @@ internal class ApkBackupTest : BackupTest() { expectChecks(packageMetadata) - assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) } @Test @@ -83,7 +83,7 @@ internal class ApkBackupTest : BackupTest() { expectChecks() assertThrows(IOException::class.java) { - assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) } } @@ -94,7 +94,7 @@ internal class ApkBackupTest : BackupTest() { every { sigInfo.hasMultipleSigners() } returns false every { sigInfo.signingCertificateHistory } returns emptyArray() - assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) } @Test @@ -120,7 +120,7 @@ internal class ApkBackupTest : BackupTest() { every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata, outputStream) } just Runs - assertEquals(updatedMetadata, apkBackup.backupApkIfNecessary(packageInfo, streamGetter)) + assertEquals(updatedMetadata, apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter)) assertArrayEquals(apkBytes, apkOutputStream.toByteArray()) } diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index e8497d89..f1c3a5bc 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -1,14 +1,15 @@ package com.stevesoltys.seedvault.transport.backup import android.app.backup.BackupTransport.* +import android.content.pm.PackageInfo import android.net.Uri import android.os.ParcelFileDescriptor import androidx.documentfile.provider.DocumentFile import com.stevesoltys.seedvault.BackupNotificationManager +import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.metadata.PackageMetadata -import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA -import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED +import com.stevesoltys.seedvault.metadata.PackageState.* import com.stevesoltys.seedvault.settings.Storage import io.mockk.* import org.junit.jupiter.api.Assertions.assertEquals @@ -24,9 +25,10 @@ internal class BackupCoordinatorTest : BackupTest() { private val kv = mockk() private val full = mockk() private val apkBackup = mockk() + private val packageService: PackageService = mockk() private val notificationManager = mockk() - private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, metadataManager, settingsManager, notificationManager) + private val backup = BackupCoordinator(context, plugin, kv, full, apkBackup, clock, packageService, metadataManager, settingsManager, notificationManager) private val metadataOutputStream = mockk() private val fileDescriptor: ParcelFileDescriptor = mockk() @@ -168,7 +170,7 @@ internal class BackupCoordinatorTest : BackupTest() { @Test fun `metadata does not get updated when no APK was backed up`() { every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK - every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns null + every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, fileDescriptor, 0)) } @@ -222,8 +224,39 @@ internal class BackupCoordinatorTest : BackupTest() { } } + @Test + fun `not allowed apps get their APKs backed up during @pm@ backup`() { + val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } + val packageName1 = "org.example.1" + val packageName2 = "org.example.2" + val notAllowedPackages = listOf( + PackageInfo().apply { packageName = packageName1 }, + PackageInfo().apply { packageName = packageName2 } + ) + val packageMetadata: PackageMetadata = mockk() + + every { settingsManager.getStorage() } returns storage // to check for removable storage + every { packageService.notAllowedPackages } returns notAllowedPackages + // no backup needed + every { apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) } returns null + // was backed up, get new packageMetadata + every { apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) } returns packageMetadata + every { plugin.getMetadataOutputStream() } returns metadataOutputStream + every { metadataManager.onApkBackedUp(packageName2, packageMetadata, metadataOutputStream) } just Runs + // do actual @pm@ backup + every { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK + + assertEquals(TRANSPORT_OK, + backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)) + + verify { + apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) + apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) + } + } + private fun expectApkBackupAndMetadataWrite() { - every { apkBackup.backupApkIfNecessary(packageInfo, any()) } returns packageMetadata + every { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns packageMetadata every { plugin.getMetadataOutputStream() } returns metadataOutputStream every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs }