Treat stopped apps different from opt-out apps

Apps that have FLAG_STOPPED will not get backed up, just like apps
without flag ALLOW_BACKUP will not get backed up.
In the UI both cases are shown the same way: app does not allow backup
This can be confusing for the user as it is not true for stopped apps.
Therefore, this commit introduces a new stopped state for apps,
so we can differentiate between both cases.
This commit is contained in:
Torsten Grote 2020-09-18 16:16:45 -03:00 committed by Chirayu Desai
parent 3e176c8e1c
commit 77550a9860
14 changed files with 118 additions and 38 deletions

View file

@ -1,5 +1,6 @@
package com.stevesoltys.seedvault.metadata package com.stevesoltys.seedvault.metadata
import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.os.Build import android.os.Build
import com.stevesoltys.seedvault.header.VERSION import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
@ -39,6 +40,10 @@ 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 the app has [FLAG_STOPPED].
*/
WAS_STOPPED,
/** /**
* Package data could not get backed up, because it was not allowed. * 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. * Most often, this is a manifest opt-out, but it could also be a disabled or system-user app.

View file

@ -13,6 +13,7 @@ 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 com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.transport.backup.isSystemApp import com.stevesoltys.seedvault.transport.backup.isSystemApp
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
@ -84,8 +85,11 @@ class MetadataManager(
} }
val oldPackageMetadata = metadata.packageMetadataMap[packageName] val oldPackageMetadata = metadata.packageMetadataMap[packageName]
?: PackageMetadata() ?: PackageMetadata()
// only allow state change if backup of this package is not allowed // only allow state change if backup of this package is not allowed,
val newState = if (packageMetadata.state == NOT_ALLOWED) { // because we need to change from the default of UNKNOWN_ERROR here,
// but otherwise don't want to modify the state since set elsewhere.
val newState =
if (packageMetadata.state == NOT_ALLOWED || packageMetadata.state == WAS_STOPPED) {
packageMetadata.state packageMetadata.state
} else { } else {
oldPackageMetadata.state oldPackageMetadata.state

View file

@ -9,6 +9,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -69,6 +70,7 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
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 NOT_ALLOWED.name -> NOT_ALLOWED
WAS_STOPPED.name -> WAS_STOPPED
else -> UNKNOWN_ERROR else -> UNKNOWN_ERROR
} }
val pSystem = p.optBoolean(JSON_PACKAGE_SYSTEM, false) val pSystem = p.optBoolean(JSON_PACKAGE_SYSTEM, false)

View file

@ -71,7 +71,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
enum class AppRestoreStatus { enum class AppRestoreStatus {
IN_PROGRESS, IN_PROGRESS,
SUCCEEDED, SUCCEEDED,
NOT_ELIGIBLE, NOT_YET_BACKED_UP,
FAILED, FAILED,
FAILED_NO_DATA, FAILED_NO_DATA,
FAILED_NOT_ALLOWED, FAILED_NOT_ALLOWED,

View file

@ -25,12 +25,14 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.restore.AppRestoreStatus.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
@ -218,6 +220,7 @@ internal class RestoreViewModel(
val metadata = restorableBackup.packageMetadataMap[packageName] ?: return FAILED val metadata = restorableBackup.packageMetadataMap[packageName] ?: return FAILED
return when (metadata.state) { return when (metadata.state) {
NO_DATA -> FAILED_NO_DATA NO_DATA -> FAILED_NO_DATA
WAS_STOPPED -> NOT_YET_BACKED_UP
NOT_ALLOWED -> FAILED_NOT_ALLOWED NOT_ALLOWED -> FAILED_NOT_ALLOWED
QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
UNKNOWN_ERROR -> FAILED UNKNOWN_ERROR -> FAILED

View file

@ -23,11 +23,12 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_ALLOWED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.NOT_ELIGIBLE import com.stevesoltys.seedvault.restore.AppRestoreStatus.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.transport.requestBackup
@ -97,9 +98,10 @@ internal class SettingsViewModel(
val status = when (metadata?.state) { val status = when (metadata?.state) {
null -> { null -> {
Log.w(TAG, "No metadata available for: ${it.packageName}") Log.w(TAG, "No metadata available for: ${it.packageName}")
NOT_ELIGIBLE NOT_YET_BACKED_UP
} }
NO_DATA -> FAILED_NO_DATA NO_DATA -> FAILED_NO_DATA
WAS_STOPPED -> NOT_YET_BACKED_UP
NOT_ALLOWED -> FAILED_NOT_ALLOWED NOT_ALLOWED -> FAILED_NOT_ALLOWED
QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
UNKNOWN_ERROR -> FAILED UNKNOWN_ERROR -> FAILED

View file

@ -63,6 +63,7 @@ fun requestBackup(context: Context) {
val observer = NotificationBackupObserver(context, packages.size, appTotals) val observer = NotificationBackupObserver(context, packages.size, appTotals)
val result = try { val result = try {
val backupManager: IBackupManager = get().koin.get() val backupManager: IBackupManager = get().koin.get()
// TODO check why this is not doing incremental K/V backups like `bmgr backupnow`
backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED) backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED)
} catch (e: RemoteException) { } catch (e: RemoteException) {
Log.e(TAG, "Error during backup: ", e) Log.e(TAG, "Error during backup: ", e)

View file

@ -10,6 +10,8 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
@ -19,6 +21,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import java.io.IOException import java.io.IOException
@ -299,34 +302,54 @@ internal class BackupCoordinator(
else -> throw IllegalStateException("Unexpected state in finishBackup()") else -> throw IllegalStateException("Unexpected state in finishBackup()")
} }
private suspend fun backUpNotAllowedPackages() { @VisibleForTesting(otherwise = PRIVATE)
internal suspend fun backUpNotAllowedPackages() {
Log.d(TAG, "Checking if APKs of opt-out apps need backup...") Log.d(TAG, "Checking if APKs of opt-out apps need backup...")
val notAllowedPackages = packageService.notAllowedPackages val notAllowedPackages = packageService.notAllowedPackages
notAllowedPackages.forEachIndexed { i, optOutPackageInfo -> notAllowedPackages.forEachIndexed { i, packageInfo ->
val packageName = packageInfo.packageName
try { try {
nm.onOptOutAppBackup(optOutPackageInfo.packageName, i + 1, notAllowedPackages.size) nm.onOptOutAppBackup(packageName, i + 1, notAllowedPackages.size)
backUpApk(optOutPackageInfo, NOT_ALLOWED) val packageState = if (packageInfo.isStopped()) WAS_STOPPED else NOT_ALLOWED
val wasBackedUp = backUpApk(packageInfo, packageState)
if (!wasBackedUp) {
val packageMetadata = metadataManager.getPackageMetadata(packageName)
val oldPackageState = packageMetadata?.state
if (oldPackageState != null && oldPackageState != packageState) {
Log.e(TAG, "Package $packageName was in $oldPackageState, update to $packageState")
plugin.getMetadataOutputStream().use {
metadataManager.onPackageBackupError(packageInfo, packageState, it)
}
}
}
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error backing up opt-out APK of ${optOutPackageInfo.packageName}", e) Log.e(TAG, "Error backing up opt-out APK of $packageName", e)
} }
} }
} }
/**
* Backs up an APK for the given [PackageInfo].
*
* @return true if a backup was performed and false if no backup was needed or it failed.
*/
private suspend fun backUpApk( private suspend fun backUpApk(
packageInfo: PackageInfo, packageInfo: PackageInfo,
packageState: PackageState = UNKNOWN_ERROR packageState: PackageState = UNKNOWN_ERROR
) { ): Boolean {
val packageName = packageInfo.packageName val packageName = packageInfo.packageName
try { return try {
apkBackup.backupApkIfNecessary(packageInfo, packageState) { apkBackup.backupApkIfNecessary(packageInfo, packageState) {
plugin.getApkOutputStream(packageInfo) plugin.getApkOutputStream(packageInfo)
}?.let { packageMetadata -> }?.let { packageMetadata ->
plugin.getMetadataOutputStream().use { plugin.getMetadataOutputStream().use {
metadataManager.onApkBackedUp(packageInfo, packageMetadata, it) metadataManager.onApkBackedUp(packageInfo, packageMetadata, it)
} }
} true
} ?: false
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Error while writing APK or metadata for $packageName", e) Log.e(TAG, "Error while writing APK or metadata for $packageName", e)
false
} }
} }

View file

@ -157,3 +157,8 @@ internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
} }
internal fun PackageInfo.isStopped(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
return applicationInfo.flags and FLAG_STOPPED != 0
}

View file

@ -19,7 +19,7 @@ import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NOT_INSTALLED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_NO_DATA
import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.FAILED_QUOTA_EXCEEDED
import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.restore.AppRestoreStatus.NOT_ELIGIBLE import com.stevesoltys.seedvault.restore.AppRestoreStatus.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
@ -64,7 +64,7 @@ internal open class AppViewHolder(protected val v: View) : RecyclerView.ViewHold
} }
private fun AppRestoreStatus.getInfo(): String = when (this) { private fun AppRestoreStatus.getInfo(): String = when (this) {
NOT_ELIGIBLE -> context.getString(R.string.restore_app_not_eligible) NOT_YET_BACKED_UP -> context.getString(R.string.restore_app_not_yet_backed_up)
FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data) FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data)
FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed) FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed)
FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed) FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed)

View file

@ -106,7 +106,7 @@
<string name="restore_restoring">Restoring backup</string> <string name="restore_restoring">Restoring backup</string>
<string name="restore_magic_package">System package manager</string> <string name="restore_magic_package">System package manager</string>
<!-- This text gets shown for apps that the OS did not try to backup for whatever reason e.g. no backup was run yet --> <!-- This text gets shown for apps that the OS did not try to backup for whatever reason e.g. no backup was run yet -->
<string name="restore_app_not_eligible">Not yet backed up</string> <string name="restore_app_not_yet_backed_up">Not yet backed up</string>
<string name="restore_app_no_data">App reported no data for backup</string> <string name="restore_app_no_data">App reported no data for backup</string>
<string name="restore_app_not_allowed">App doesn\'t allow backup</string> <string name="restore_app_not_allowed">App doesn\'t allow backup</string>
<string name="restore_app_not_installed">App not installed</string> <string name="restore_app_not_installed">App not installed</string>

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import io.mockk.Runs import io.mockk.Runs
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
@ -163,6 +164,11 @@ class MetadataManagerTest {
packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED) packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED)
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream) manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
assertEquals(packageMetadata.copy(state = NOT_ALLOWED), manager.getPackageMetadata(packageName)) assertEquals(packageMetadata.copy(state = NOT_ALLOWED), manager.getPackageMetadata(packageName))
// state DOES change for WAS_STOPPED
packageMetadata = packageMetadata.copy(version = ++version, state = WAS_STOPPED)
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
assertEquals(packageMetadata.copy(state = WAS_STOPPED), manager.getPackageMetadata(packageName))
} }
@Test @Test

View file

@ -6,6 +6,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import io.mockk.mockk import io.mockk.mockk
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -32,6 +33,7 @@ internal class MetadataWriterDecoderTest {
val time = Random.nextLong() val time = Random.nextLong()
val packages = HashMap<String, PackageMetadata>().apply { val packages = HashMap<String, PackageMetadata>().apply {
put(getRandomString(), PackageMetadata(time, APK_AND_DATA)) put(getRandomString(), PackageMetadata(time, APK_AND_DATA))
put(getRandomString(), PackageMetadata(time, WAS_STOPPED))
} }
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))

View file

@ -4,6 +4,8 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
@ -16,6 +18,7 @@ import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED import com.stevesoltys.seedvault.metadata.PackageState.QUOTA_EXCEEDED
import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR import com.stevesoltys.seedvault.metadata.PackageState.UNKNOWN_ERROR
import com.stevesoltys.seedvault.metadata.PackageState.WAS_STOPPED
import com.stevesoltys.seedvault.settings.Storage import com.stevesoltys.seedvault.settings.Storage
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import io.mockk.Runs import io.mockk.Runs
@ -318,7 +321,11 @@ internal class BackupCoordinatorTest : BackupTest() {
val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER } val packageInfo = PackageInfo().apply { packageName = MAGIC_PACKAGE_MANAGER }
val notAllowedPackages = listOf( val notAllowedPackages = listOf(
PackageInfo().apply { packageName = "org.example.1" }, PackageInfo().apply { packageName = "org.example.1" },
PackageInfo().apply { packageName = "org.example.2" } PackageInfo().apply {
packageName = "org.example.2"
// the second package does not get backed up, because it is stopped
applicationInfo = ApplicationInfo().apply { flags = FLAG_STOPPED }
}
) )
val packageMetadata: PackageMetadata = mockk() val packageMetadata: PackageMetadata = mockk()
@ -337,13 +344,13 @@ internal class BackupCoordinatorTest : BackupTest() {
} just Runs } just Runs
// no backup needed // no backup needed
coEvery { coEvery {
apkBackup.backupApkIfNecessary( apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
notAllowedPackages[0],
NOT_ALLOWED,
any()
)
} returns null } returns null
// update notification // check old metadata for state changes, because we won't update it otherwise
every { metadataManager.getPackageMetadata(notAllowedPackages[0].packageName) } returns packageMetadata
every { packageMetadata.state } returns NOT_ALLOWED // no change
// update notification for second package
every { every {
notificationManager.onOptOutAppBackup( notificationManager.onOptOutAppBackup(
notAllowedPackages[1].packageName, notAllowedPackages[1].packageName,
@ -353,11 +360,7 @@ internal class BackupCoordinatorTest : BackupTest() {
} just Runs } just Runs
// was backed up, get new packageMetadata // was backed up, get new packageMetadata
coEvery { coEvery {
apkBackup.backupApkIfNecessary( apkBackup.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any())
notAllowedPackages[1],
NOT_ALLOWED,
any()
)
} returns packageMetadata } returns packageMetadata
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
every { every {
@ -369,14 +372,38 @@ internal class BackupCoordinatorTest : BackupTest() {
} just Runs } just Runs
every { metadataOutputStream.close() } just Runs every { metadataOutputStream.close() } just Runs
assertEquals( assertEquals(TRANSPORT_OK, backup.performIncrementalBackup(packageInfo, fileDescriptor, 0))
TRANSPORT_OK,
backup.performIncrementalBackup(packageInfo, fileDescriptor, 0)
)
coVerify { coVerify {
apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any()) apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
apkBackup.backupApkIfNecessary(notAllowedPackages[1], NOT_ALLOWED, any()) apkBackup.backupApkIfNecessary(notAllowedPackages[1], WAS_STOPPED, any())
metadataOutputStream.close()
}
}
@Test
fun `APK backup of not allowed apps updates state even without new APK`() = runBlocking {
val oldPackageMetadata: PackageMetadata = mockk()
every { packageService.notAllowedPackages } returns listOf(packageInfo)
every { notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1) } just Runs
coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns oldPackageMetadata
every { oldPackageMetadata.state } returns WAS_STOPPED // state differs now, was stopped before
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
every {
metadataManager.onPackageBackupError(
packageInfo,
NOT_ALLOWED,
metadataOutputStream
)
} just Runs
every { metadataOutputStream.close() } just Runs
backup.backUpNotAllowedPackages()
verify {
metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)
metadataOutputStream.close() metadataOutputStream.close()
} }
} }