Also back-up APKs of apps that are not allowed to have their data backed up

This commit is contained in:
Torsten Grote 2020-01-13 15:46:07 -03:00
parent 3d296e1335
commit fea53a759f
No known key found for this signature in database
GPG key ID: 3E5F77D92CF891FF
13 changed files with 169 additions and 37 deletions

View file

@ -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.
*/

View file

@ -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,

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -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<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
single { FullBackup(get<BackupPlugin>().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()) }
}

View file

@ -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<String>
@WorkerThread
@Throws(RemoteException::class)
get() {
val packages: List<PackageInfo> = packageManager.getInstalledPackages(0, UserHandle.USER_SYSTEM).list as List<PackageInfo>
val packages: List<PackageInfo> = packageManagerService.getInstalledPackages(0, UserHandle.USER_SYSTEM).list as List<PackageInfo>
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<PackageInfo>
@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
}
}
}

View file

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

View file

@ -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<String, PackageMetadata>().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))

View file

@ -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<FullBackupPlugin>()
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
private val apkBackup = mockk<ApkBackup>()
private val packageService:PackageService = mockk()
private val notificationManager = mockk<BackupNotificationManager>()
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<RestorePlugin>()
private val kvRestorePlugin = mockk<KVRestorePlugin>()
@ -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

View file

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

View file

@ -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<KVBackup>()
private val full = mockk<FullBackup>()
private val apkBackup = mockk<ApkBackup>()
private val packageService: PackageService = mockk()
private val notificationManager = mockk<BackupNotificationManager>()
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<OutputStream>()
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
}