Also back-up APKs of apps that are not allowed to have their data backed up
This commit is contained in:
parent
3d296e1335
commit
fea53a759f
13 changed files with 169 additions and 37 deletions
|
@ -39,6 +39,11 @@ enum class PackageState {
|
||||||
* Package data could not get backed up, because the app reported no data to back up.
|
* Package data could not get backed up, because the app reported no data to back up.
|
||||||
*/
|
*/
|
||||||
NO_DATA,
|
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.
|
* Package data could not get backed up, because an error occurred during backup.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import com.stevesoltys.seedvault.Clock
|
import com.stevesoltys.seedvault.Clock
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.OutputStream
|
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}"
|
"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) {
|
modifyMetadata(metadataOutputStream) {
|
||||||
val oldPackageMetadata = metadata.packageMetadataMap[packageName]
|
|
||||||
?: PackageMetadata()
|
|
||||||
metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy(
|
metadata.packageMetadataMap[packageName] = oldPackageMetadata.copy(
|
||||||
|
state = newState,
|
||||||
version = packageMetadata.version,
|
version = packageMetadata.version,
|
||||||
installer = packageMetadata.installer,
|
installer = packageMetadata.installer,
|
||||||
sha256 = packageMetadata.sha256,
|
sha256 = packageMetadata.sha256,
|
||||||
|
|
|
@ -64,6 +64,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
"" -> APK_AND_DATA
|
"" -> APK_AND_DATA
|
||||||
QUOTA_EXCEEDED.name -> QUOTA_EXCEEDED
|
QUOTA_EXCEEDED.name -> QUOTA_EXCEEDED
|
||||||
NO_DATA.name -> NO_DATA
|
NO_DATA.name -> NO_DATA
|
||||||
|
NOT_ALLOWED.name -> NOT_ALLOWED
|
||||||
else -> UNKNOWN_ERROR
|
else -> UNKNOWN_ERROR
|
||||||
}
|
}
|
||||||
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
|
val pVersion = p.optLong(JSON_PACKAGE_VERSION, 0L)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import com.stevesoltys.seedvault.BackupMonitor
|
||||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
import com.stevesoltys.seedvault.NotificationBackupObserver
|
import com.stevesoltys.seedvault.NotificationBackupObserver
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
import com.stevesoltys.seedvault.transport.backup.PackageService
|
||||||
import org.koin.core.context.GlobalContext.get
|
import org.koin.core.context.GlobalContext.get
|
||||||
|
|
||||||
private val TAG = ConfigurableBackupTransportService::class.java.simpleName
|
private val TAG = ConfigurableBackupTransportService::class.java.simpleName
|
||||||
|
@ -55,9 +56,10 @@ fun requestBackup(context: Context) {
|
||||||
val nm: BackupNotificationManager = get().koin.get()
|
val nm: BackupNotificationManager = get().koin.get()
|
||||||
nm.onBackupUpdate(context.getString(R.string.notification_backup_starting), 0, 1, true)
|
nm.onBackupUpdate(context.getString(R.string.notification_backup_starting), 0, 1, true)
|
||||||
|
|
||||||
|
val packageService: PackageService = get().koin.get()
|
||||||
val observer = NotificationBackupObserver(context, true)
|
val observer = NotificationBackupObserver(context, true)
|
||||||
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
|
val flags = FLAG_NON_INCREMENTAL_BACKUP or FLAG_USER_INITIATED
|
||||||
val packages = PackageService.eligiblePackages
|
val packages = packageService.eligiblePackages
|
||||||
val result = try {
|
val result = try {
|
||||||
val backupManager: IBackupManager = get().koin.get()
|
val backupManager: IBackupManager = get().koin.get()
|
||||||
backupManager.requestBackup(packages, observer, BackupMonitor(), flags)
|
backupManager.requestBackup(packages, observer, BackupMonitor(), flags)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.encodeBase64
|
import com.stevesoltys.seedvault.encodeBase64
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataManager
|
import com.stevesoltys.seedvault.metadata.MetadataManager
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageState
|
||||||
import com.stevesoltys.seedvault.settings.SettingsManager
|
import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
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.
|
* @return new [PackageMetadata] if an APK backup was made or null if no backup was made.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
fun backupApkIfNecessary(packageInfo: PackageInfo, streamGetter: () -> OutputStream): PackageMetadata? {
|
fun backupApkIfNecessary(packageInfo: PackageInfo, packageState: PackageState, streamGetter: () -> OutputStream): PackageMetadata? {
|
||||||
// do not back up @pm@
|
// do not back up @pm@
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER) return null
|
if (packageName == MAGIC_PACKAGE_MANAGER) return null
|
||||||
|
@ -108,6 +109,7 @@ class ApkBackup(
|
||||||
|
|
||||||
// return updated metadata
|
// return updated metadata
|
||||||
return PackageMetadata(
|
return PackageMetadata(
|
||||||
|
state = packageState,
|
||||||
version = version,
|
version = version,
|
||||||
installer = pm.getInstallerPackageName(packageName),
|
installer = pm.getInstallerPackageName(packageName),
|
||||||
sha256 = sha256,
|
sha256 = sha256,
|
||||||
|
|
|
@ -28,6 +28,7 @@ internal class BackupCoordinator(
|
||||||
private val full: FullBackup,
|
private val full: FullBackup,
|
||||||
private val apkBackup: ApkBackup,
|
private val apkBackup: ApkBackup,
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
private val packageService: PackageService,
|
||||||
private val metadataManager: MetadataManager,
|
private val metadataManager: MetadataManager,
|
||||||
private val settingsManager: SettingsManager,
|
private val settingsManager: SettingsManager,
|
||||||
private val nm: BackupNotificationManager) {
|
private val nm: BackupNotificationManager) {
|
||||||
|
@ -115,10 +116,14 @@ internal class BackupCoordinator(
|
||||||
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
fun performIncrementalBackup(packageInfo: PackageInfo, data: ParcelFileDescriptor, flags: Int): Int {
|
||||||
cancelReason = UNKNOWN_ERROR
|
cancelReason = UNKNOWN_ERROR
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
// backups of package manager metadata do not respect backoff
|
if (packageName == MAGIC_PACKAGE_MANAGER) {
|
||||||
// we need to reject them manually when now is not a good time for a backup
|
// backups of package manager metadata do not respect backoff
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER && getBackupBackoff() != 0L) {
|
// we need to reject them manually when now is not a good time for a backup
|
||||||
return TRANSPORT_PACKAGE_REJECTED
|
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)
|
val result = kv.performBackup(packageInfo, data, flags)
|
||||||
return backUpApk(result, packageInfo)
|
return backUpApk(result, packageInfo)
|
||||||
|
@ -238,10 +243,22 @@ internal class BackupCoordinator(
|
||||||
else -> throw IllegalStateException("Unexpected state in finishBackup()")
|
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
|
val packageName = packageInfo.packageName
|
||||||
|
if (packageName == MAGIC_PACKAGE_MANAGER) return result
|
||||||
return try {
|
return try {
|
||||||
apkBackup.backupApkIfNecessary(packageInfo) {
|
apkBackup.backupApkIfNecessary(packageInfo, packageState) {
|
||||||
plugin.getApkOutputStream(packageInfo)
|
plugin.getApkOutputStream(packageInfo)
|
||||||
}?.let { packageMetadata ->
|
}?.let { packageMetadata ->
|
||||||
val outputStream = plugin.getMetadataOutputStream()
|
val outputStream = plugin.getMetadataOutputStream()
|
||||||
|
|
|
@ -5,8 +5,9 @@ import org.koin.dsl.module
|
||||||
|
|
||||||
val backupModule = module {
|
val backupModule = module {
|
||||||
single { InputFactory() }
|
single { InputFactory() }
|
||||||
|
single { PackageService(androidContext().packageManager, get()) }
|
||||||
single { ApkBackup(androidContext().packageManager, get(), get()) }
|
single { ApkBackup(androidContext().packageManager, get(), get()) }
|
||||||
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
|
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get()) }
|
||||||
single { FullBackup(get<BackupPlugin>().fullBackupPlugin, 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()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
package com.stevesoltys.seedvault.transport
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.pm.IPackageManager
|
import android.content.pm.IPackageManager
|
||||||
import android.content.pm.PackageInfo
|
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.RemoteException
|
||||||
import android.os.ServiceManager.getService
|
import android.os.ServiceManager.getService
|
||||||
import android.os.UserHandle
|
import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Log.INFO
|
import android.util.Log.INFO
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
import com.google.android.collect.Sets.newArraySet
|
import com.google.android.collect.Sets.newArraySet
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import org.koin.core.KoinComponent
|
|
||||||
import org.koin.core.inject
|
|
||||||
|
|
||||||
private val TAG = PackageService::class.java.simpleName
|
private val TAG = PackageService::class.java.simpleName
|
||||||
|
|
||||||
|
@ -30,15 +31,19 @@ private val IGNORED_PACKAGES = newArraySet(
|
||||||
* @author Steve Soltys
|
* @author Steve Soltys
|
||||||
* @author Torsten Grote
|
* @author Torsten Grote
|
||||||
*/
|
*/
|
||||||
internal object PackageService : KoinComponent {
|
internal class PackageService(
|
||||||
|
private val packageManager: PackageManager,
|
||||||
|
private val backupManager: IBackupManager) {
|
||||||
|
|
||||||
private val backupManager: IBackupManager by inject()
|
// TODO This can probably be removed and PackageManager#getInstalledPackages() used instead
|
||||||
private val packageManager: IPackageManager = IPackageManager.Stub.asInterface(getService("package"))
|
private val packageManagerService: IPackageManager = IPackageManager.Stub.asInterface(getService("package"))
|
||||||
|
private val myUserId = UserHandle.myUserId()
|
||||||
|
|
||||||
val eligiblePackages: Array<String>
|
val eligiblePackages: Array<String>
|
||||||
|
@WorkerThread
|
||||||
@Throws(RemoteException::class)
|
@Throws(RemoteException::class)
|
||||||
get() {
|
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
|
val packageList = packages
|
||||||
.map { packageInfo -> packageInfo.packageName }
|
.map { packageInfo -> packageInfo.packageName }
|
||||||
.filter { packageName -> !IGNORED_PACKAGES.contains(packageName) }
|
.filter { packageName -> !IGNORED_PACKAGES.contains(packageName) }
|
||||||
|
@ -53,7 +58,7 @@ internal object PackageService : KoinComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO why is this filtering out so much?
|
// 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
|
// log eligible packages
|
||||||
if (Log.isLoggable(TAG, INFO)) {
|
if (Log.isLoggable(TAG, INFO)) {
|
||||||
|
@ -70,4 +75,19 @@ internal object PackageService : KoinComponent {
|
||||||
return packageArray.toTypedArray()
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.stevesoltys.seedvault.Clock
|
import com.stevesoltys.seedvault.Clock
|
||||||
import com.stevesoltys.seedvault.getRandomByteArray
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
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.Runs
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -97,6 +97,40 @@ class MetadataManagerTest {
|
||||||
assertEquals(updatedPackageMetadata, manager.getPackageMetadata(packageName))
|
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
|
@Test
|
||||||
fun `test onPackageBackedUp()`() {
|
fun `test onPackageBackedUp()`() {
|
||||||
val updatedMetadata = initialMetadata.copy()
|
val updatedMetadata = initialMetadata.copy()
|
||||||
|
|
|
@ -50,7 +50,7 @@ internal class MetadataWriterDecoderTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@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 {
|
val packages = HashMap<String, PackageMetadata>().apply {
|
||||||
put(getRandomString(), PackageMetadata(
|
put(getRandomString(), PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
|
@ -68,6 +68,14 @@ internal class MetadataWriterDecoderTest {
|
||||||
sha256 = getRandomString(),
|
sha256 = getRandomString(),
|
||||||
signatures = listOf(getRandomString(), 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)
|
val metadata = getMetadata(packages)
|
||||||
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.stevesoltys.seedvault.header.HeaderWriterImpl
|
||||||
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
import com.stevesoltys.seedvault.header.MAX_SEGMENT_CLEARTEXT_LENGTH
|
||||||
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
import com.stevesoltys.seedvault.metadata.MetadataReaderImpl
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
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.backup.*
|
||||||
import com.stevesoltys.seedvault.transport.restore.*
|
import com.stevesoltys.seedvault.transport.restore.*
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
|
@ -43,8 +44,9 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
|
private val packageService:PackageService = mockk()
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
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 restorePlugin = mockk<RestorePlugin>()
|
||||||
private val kvRestorePlugin = mockk<KVRestorePlugin>()
|
private val kvRestorePlugin = mockk<KVRestorePlugin>()
|
||||||
|
@ -94,7 +96,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
appData2.size
|
appData2.size
|
||||||
}
|
}
|
||||||
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key264) } returns bOutputStream2
|
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 { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
||||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||||
|
@ -148,7 +150,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
appData.size
|
appData.size
|
||||||
}
|
}
|
||||||
every { kvBackupPlugin.getOutputStreamForRecord(packageInfo, key64) } returns bOutputStream
|
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 { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||||
|
|
||||||
|
@ -186,7 +188,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
every { fullBackupPlugin.getOutputStream(packageInfo) } returns bOutputStream
|
||||||
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
every { inputFactory.getInputStream(fileDescriptor) } returns bInputStream
|
||||||
every { fullBackupPlugin.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
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 { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
||||||
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
every { metadataManager.onPackageBackedUp(packageInfo.packageName, metadataOutputStream) } just Runs
|
||||||
|
|
|
@ -45,14 +45,14 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `does not back up @pm@`() {
|
fun `does not back up @pm@`() {
|
||||||
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
|
||||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `does not back up when setting disabled`() {
|
fun `does not back up when setting disabled`() {
|
||||||
every { settingsManager.backupApks() } returns false
|
every { settingsManager.backupApks() } returns false
|
||||||
|
|
||||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -61,7 +61,7 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
|
|
||||||
every { settingsManager.backupApks() } returns true
|
every { settingsManager.backupApks() } returns true
|
||||||
|
|
||||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -73,7 +73,7 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
|
|
||||||
expectChecks(packageMetadata)
|
expectChecks(packageMetadata)
|
||||||
|
|
||||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -83,7 +83,7 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
expectChecks()
|
expectChecks()
|
||||||
|
|
||||||
assertThrows(IOException::class.java) {
|
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.hasMultipleSigners() } returns false
|
||||||
every { sigInfo.signingCertificateHistory } returns emptyArray()
|
every { sigInfo.signingCertificateHistory } returns emptyArray()
|
||||||
|
|
||||||
assertNull(apkBackup.backupApkIfNecessary(packageInfo, streamGetter))
|
assertNull(apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, streamGetter))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -120,7 +120,7 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer
|
every { pm.getInstallerPackageName(packageInfo.packageName) } returns updatedMetadata.installer
|
||||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, updatedMetadata, outputStream) } just Runs
|
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())
|
assertArrayEquals(apkBytes, apkOutputStream.toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
package com.stevesoltys.seedvault.transport.backup
|
package com.stevesoltys.seedvault.transport.backup
|
||||||
|
|
||||||
import android.app.backup.BackupTransport.*
|
import android.app.backup.BackupTransport.*
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.stevesoltys.seedvault.BackupNotificationManager
|
import com.stevesoltys.seedvault.BackupNotificationManager
|
||||||
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.getRandomString
|
import com.stevesoltys.seedvault.getRandomString
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.*
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
|
|
||||||
import com.stevesoltys.seedvault.settings.Storage
|
import com.stevesoltys.seedvault.settings.Storage
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
@ -24,9 +25,10 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
private val kv = mockk<KVBackup>()
|
private val kv = mockk<KVBackup>()
|
||||||
private val full = mockk<FullBackup>()
|
private val full = mockk<FullBackup>()
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
|
private val packageService: PackageService = mockk()
|
||||||
private val notificationManager = mockk<BackupNotificationManager>()
|
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 metadataOutputStream = mockk<OutputStream>()
|
||||||
private val fileDescriptor: ParcelFileDescriptor = mockk()
|
private val fileDescriptor: ParcelFileDescriptor = mockk()
|
||||||
|
@ -168,7 +170,7 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `metadata does not get updated when no APK was backed up`() {
|
fun `metadata does not get updated when no APK was backed up`() {
|
||||||
every { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
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))
|
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() {
|
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 { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
every { metadataManager.onApkBackedUp(packageInfo.packageName, packageMetadata, metadataOutputStream) } just Runs
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue