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:
parent
3e176c8e1c
commit
77550a9860
14 changed files with 118 additions and 38 deletions
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue