Show percentages in progress notification and x of n status at the end

Fine-grained progress reporting causes apps to show up twice which is
confusing. Also @pm@ metadata and opt-out APKs are too much detail for
normal users. So we decided to only show a percentage in the progress
notification.

When the backup finished, the app now shows "x of n apps backed up"
which is more positive when the previous negative message of how many
apps were not backed up.

Some further minor tweets were done to app counting to report proper
totals.
This commit is contained in:
Torsten Grote 2020-08-28 15:22:17 -03:00 committed by Chirayu Desai
parent 983f917391
commit 80187c8c70
20 changed files with 155 additions and 91 deletions

View file

@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.SettingsViewModel import com.stevesoltys.seedvault.settings.SettingsViewModel
import com.stevesoltys.seedvault.transport.backup.backupModule import com.stevesoltys.seedvault.transport.backup.backupModule
import com.stevesoltys.seedvault.transport.restore.restoreModule import com.stevesoltys.seedvault.transport.restore.restoreModule
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel import com.stevesoltys.seedvault.ui.recoverycode.RecoveryCodeViewModel
import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel import com.stevesoltys.seedvault.ui.storage.BackupStorageViewModel
import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel
@ -36,7 +37,7 @@ class App : Application() {
single { Clock() } single { Clock() }
factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) } factory<IBackupManager> { IBackupManager.Stub.asInterface(getService(BACKUP_SERVICE)) }
viewModel { SettingsViewModel(this@App, get(), get(), get()) } viewModel { SettingsViewModel(this@App, get(), get(), get(), get()) }
viewModel { RecoveryCodeViewModel(this@App, get()) } viewModel { RecoveryCodeViewModel(this@App, get()) }
viewModel { BackupStorageViewModel(this@App, get(), get()) } viewModel { BackupStorageViewModel(this@App, get(), get()) }
viewModel { RestoreStorageViewModel(this@App, get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) }

View file

@ -12,6 +12,7 @@ import androidx.lifecycle.distinctUntilChanged
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 com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
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
@ -196,9 +197,12 @@ class MetadataManager(
} }
@Synchronized @Synchronized
fun getPackagesNumNotBackedUp(): Int { fun getPackagesNumBackedUp(): Int {
return metadata.packageMetadataMap.filter { (_, packageMetadata) -> return metadata.packageMetadataMap.filter { (_, packageMetadata) ->
!packageMetadata.system && packageMetadata.state != APK_AND_DATA !packageMetadata.system && ( // ignore system apps
packageMetadata.state == APK_AND_DATA || // either full success
packageMetadata.state == NO_DATA // or apps that simply had no data
)
}.count() }.count()
} }

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import androidx.core.net.toUri import androidx.core.net.toUri
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import org.koin.core.context.GlobalContext.get import org.koin.core.context.GlobalContext.get
internal const val ACTION_RESTORE_ERROR_UNINSTALL = "com.stevesoltys.seedvault.action.UNINSTALL" internal const val ACTION_RESTORE_ERROR_UNINSTALL = "com.stevesoltys.seedvault.action.UNINSTALL"

View file

@ -10,7 +10,7 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder
import com.stevesoltys.seedvault.ui.AppViewHolder import com.stevesoltys.seedvault.ui.AppViewHolder
import java.util.* import java.util.LinkedList
internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() { internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
@ -50,7 +50,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
} }
} }
inner class PackageViewHolder(v: View) : AppViewHolder(v) { class PackageViewHolder(v: View) : AppViewHolder(v) {
fun bind(item: AppRestoreResult) { fun bind(item: AppRestoreResult) {
appName.text = item.name appName.text = item.name
if (item.packageName == MAGIC_PACKAGE_MANAGER) { if (item.packageName == MAGIC_PACKAGE_MANAGER) {
@ -71,6 +71,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
enum class AppRestoreStatus { enum class AppRestoreStatus {
IN_PROGRESS, IN_PROGRESS,
SUCCEEDED, SUCCEEDED,
NOT_ELIGIBLE,
FAILED, FAILED,
FAILED_NO_DATA, FAILED_NO_DATA,
FAILED_NOT_ALLOWED, FAILED_NOT_ALLOWED,

View file

@ -19,7 +19,7 @@ import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.getAppName import com.stevesoltys.seedvault.ui.notification.getAppName
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

View file

@ -5,7 +5,7 @@ import androidx.annotation.CallSuper
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel

View file

@ -14,7 +14,6 @@ import androidx.recyclerview.widget.DiffUtil.calculateDiff
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.crypto.KeyManager
import com.stevesoltys.seedvault.getAppName
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
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
@ -25,21 +24,24 @@ 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.SUCCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.transport.backup.isSystemApp import com.stevesoltys.seedvault.transport.backup.PackageService
import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.transport.requestBackup
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.getAppName
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Locale import java.util.Locale
private val TAG = SettingsViewModel::class.java.simpleName private val TAG = SettingsViewModel::class.java.simpleName
class SettingsViewModel( internal class SettingsViewModel(
app: Application, app: Application,
settingsManager: SettingsManager, settingsManager: SettingsManager,
keyManager: KeyManager, keyManager: KeyManager,
private val metadataManager: MetadataManager private val metadataManager: MetadataManager,
private val packageService: PackageService
) : RequireProvisioningViewModel(app, settingsManager, keyManager) { ) : RequireProvisioningViewModel(app, settingsManager, keyManager) {
override val isRestoreOperation = false override val isRestoreOperation = false
@ -69,43 +71,41 @@ class SettingsViewModel(
private fun getAppStatusResult(): LiveData<AppStatusResult> = liveData { private fun getAppStatusResult(): LiveData<AppStatusResult> = liveData {
val pm = app.packageManager val pm = app.packageManager
val locale = Locale.getDefault() val locale = Locale.getDefault()
val list = pm.getInstalledPackages(0) val list = packageService.userApps.map {
.filter { !it.isSystemApp() } val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) {
.map { getDrawable(app, R.drawable.ic_launcher_default)!!
val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) { } else {
try {
pm.getApplicationIcon(it.packageName)
} catch (e: NameNotFoundException) {
getDrawable(app, R.drawable.ic_launcher_default)!! getDrawable(app, R.drawable.ic_launcher_default)!!
} else {
try {
pm.getApplicationIcon(it.packageName)
} catch (e: NameNotFoundException) {
getDrawable(app, R.drawable.ic_launcher_default)!!
}
} }
val metadata = metadataManager.getPackageMetadata(it.packageName) }
val time = metadata?.time ?: 0 val metadata = metadataManager.getPackageMetadata(it.packageName)
val status = when (metadata?.state) { val time = metadata?.time ?: 0
null -> { val status = when (metadata?.state) {
Log.w(TAG, "No metadata available for: ${it.packageName}") null -> {
FAILED Log.w(TAG, "No metadata available for: ${it.packageName}")
} NOT_ELIGIBLE
NO_DATA -> FAILED_NO_DATA
NOT_ALLOWED -> FAILED_NOT_ALLOWED
QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
UNKNOWN_ERROR -> FAILED
APK_AND_DATA -> SUCCEEDED
} }
if (metadata?.hasApk() == false) { NO_DATA -> FAILED_NO_DATA
Log.w(TAG, "No APK stored for: ${it.packageName}") NOT_ALLOWED -> FAILED_NOT_ALLOWED
} QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED
AppStatus( UNKNOWN_ERROR -> FAILED
packageName = it.packageName, APK_AND_DATA -> SUCCEEDED
enabled = settingsManager.isBackupEnabled(it.packageName), }
icon = icon, if (metadata?.hasApk() == false) {
name = getAppName(app, it.packageName).toString(), Log.w(TAG, "No APK stored for: ${it.packageName}")
time = time, }
status = status AppStatus(
) packageName = it.packageName,
}.sortedBy { it.name.toLowerCase(locale) } enabled = settingsManager.isBackupEnabled(it.packageName),
icon = icon,
name = getAppName(app, it.packageName).toString(),
time = time,
status = status
)
}.sortedBy { it.name.toLowerCase(locale) }
val oldList = mAppStatusList.value?.appStatusList ?: emptyList() val oldList = mAppStatusList.value?.appStatusList ?: emptyList()
val diff = calculateDiff(AppStatusDiff(oldList, list)) val diff = calculateDiff(AppStatusDiff(oldList, list))
emit(AppStatusResult(list, diff)) emit(AppStatusResult(list, diff))

View file

@ -13,8 +13,8 @@ import android.os.RemoteException
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.BackupMonitor
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.NotificationBackupObserver import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver
import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.backup.PackageService
import org.koin.core.context.GlobalContext.get import org.koin.core.context.GlobalContext.get
@ -53,9 +53,9 @@ class ConfigurableBackupTransportService : Service() {
fun requestBackup(context: Context) { fun requestBackup(context: Context) {
val packageService: PackageService = get().koin.get() val packageService: PackageService = get().koin.get()
val packages = packageService.eligiblePackages val packages = packageService.eligiblePackages
val optOutPackages = packageService.notAllowedPackages val appTotals = packageService.expectedAppTotals
val observer = NotificationBackupObserver(context, packages.size, optOutPackages.size, true) val observer = NotificationBackupObserver(context, packages.size, appTotals, true)
val result = try { val result = try {
val backupManager: IBackupManager = get().koin.get() val backupManager: IBackupManager = get().koin.get()
backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED) backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED)

View file

@ -10,7 +10,7 @@ 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.WorkerThread import androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.Clock
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager

View file

@ -8,7 +8,7 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.crypto.Crypto import com.stevesoltys.seedvault.crypto.Crypto
import com.stevesoltys.seedvault.encodeBase64 import com.stevesoltys.seedvault.encodeBase64

View file

@ -2,16 +2,19 @@ package com.stevesoltys.seedvault.transport.backup
import android.app.backup.IBackupManager import android.app.backup.IBackupManager
import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP import android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP
import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.content.pm.ApplicationInfo.FLAG_SYSTEM import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_INSTRUMENTATION
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.os.RemoteException import android.os.RemoteException
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 androidx.annotation.WorkerThread
import com.stevesoltys.seedvault.BuildConfig.APPLICATION_ID
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
private val TAG = PackageService::class.java.simpleName private val TAG = PackageService::class.java.simpleName
@ -68,8 +71,9 @@ internal class PackageService(
// because the package info is used by [ApkBackup] which needs signing info. // because the package info is used by [ApkBackup] which needs signing info.
return packageManager.getInstalledPackages(GET_SIGNING_CERTIFICATES) return packageManager.getInstalledPackages(GET_SIGNING_CERTIFICATES)
.filter { packageInfo -> .filter { packageInfo ->
!packageInfo.isBackupAllowed() && // only apps that do not allow backup packageInfo.doesNotGetBackedUp() && // only apps that do not allow backup
!packageInfo.isNotUpdatedSystemApp() // and are not vanilla system apps !packageInfo.isNotUpdatedSystemApp() && // and are not vanilla system apps
packageInfo.packageName != APPLICATION_ID // not this app
}.sortedBy { packageInfo -> }.sortedBy { packageInfo ->
packageInfo.packageName packageInfo.packageName
}.also { notAllowed -> }.also { notAllowed ->
@ -81,6 +85,32 @@ internal class PackageService(
} }
} }
/**
* A list of non-system apps (without instrumentation test apps).
*/
val userApps: List<PackageInfo>
@WorkerThread
get() {
return packageManager.getInstalledPackages(GET_INSTRUMENTATION)
.filter { it.isUserVisible() }
}
val expectedAppTotals: ExpectedAppTotals
@WorkerThread
get() {
var appsTotal = 0
var appsOptOut = 0
packageManager.getInstalledPackages(GET_INSTRUMENTATION).forEach { packageInfo ->
if (packageInfo.isUserVisible()) {
appsTotal++
if (packageInfo.doesNotGetBackedUp()) {
appsOptOut++
}
}
}
return ExpectedAppTotals(appsTotal, appsOptOut)
}
private fun logPackages(packages: List<String>) { private fun logPackages(packages: List<String>) {
packages.chunked(LOG_MAX_PACKAGES).forEach { packages.chunked(LOG_MAX_PACKAGES).forEach {
Log.i(TAG, it.toString()) Log.i(TAG, it.toString())
@ -89,6 +119,22 @@ internal class PackageService(
} }
internal data class ExpectedAppTotals(
/**
* The total number of non-system apps eligible for backup.
*/
val appsTotal: Int,
/**
* The number of non-system apps that has opted-out of backup.
*/
val appsOptOut: Int
)
internal fun PackageInfo.isUserVisible(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false
return !isNotUpdatedSystemApp() && instrumentation == null && packageName != APPLICATION_ID
}
internal fun PackageInfo.isSystemApp(): Boolean { internal fun PackageInfo.isSystemApp(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
return applicationInfo.flags and FLAG_SYSTEM != 0 return applicationInfo.flags and FLAG_SYSTEM != 0
@ -105,7 +151,8 @@ internal fun PackageInfo.isNotUpdatedSystemApp(): Boolean {
return isSystemApp && !isUpdatedSystemApp return isSystemApp && !isUpdatedSystemApp
} }
internal fun PackageInfo.isBackupAllowed(): Boolean { internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0 return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 && // does not allow backup
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
} }

View file

@ -13,7 +13,7 @@ import android.content.pm.PackageInfo
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.util.Log import android.util.Log
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.header.UnsupportedVersionException import com.stevesoltys.seedvault.header.UnsupportedVersionException

View file

@ -19,6 +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.SUCCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.SUCCEEDED
@ -63,6 +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)
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

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault.ui.notification
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -16,11 +16,14 @@ import androidx.core.app.NotificationCompat.Builder
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_HIGH import androidx.core.app.NotificationCompat.PRIORITY_HIGH
import androidx.core.app.NotificationCompat.PRIORITY_LOW import androidx.core.app.NotificationCompat.PRIORITY_LOW
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL import com.stevesoltys.seedvault.restore.ACTION_RESTORE_ERROR_UNINSTALL
import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME import com.stevesoltys.seedvault.restore.EXTRA_PACKAGE_NAME
import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL
import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST
import com.stevesoltys.seedvault.settings.SettingsActivity import com.stevesoltys.seedvault.settings.SettingsActivity
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver" private const val CHANNEL_ID_OBSERVER = "NotificationBackupObserver"
private const val CHANNEL_ID_ERROR = "NotificationError" private const val CHANNEL_ID_ERROR = "NotificationError"
@ -31,7 +34,7 @@ private const val NOTIFICATION_ID_RESTORE_ERROR = 3
private val TAG = BackupNotificationManager::class.java.simpleName private val TAG = BackupNotificationManager::class.java.simpleName
class BackupNotificationManager(private val context: Context) { internal class BackupNotificationManager(private val context: Context) {
private val nm = context.getSystemService(NotificationManager::class.java)!!.apply { private val nm = context.getSystemService(NotificationManager::class.java)!!.apply {
createNotificationChannel(getObserverChannel()) createNotificationChannel(getObserverChannel())
@ -41,6 +44,7 @@ class BackupNotificationManager(private val context: Context) {
private var expectedApps: Int? = null private var expectedApps: Int? = null
private var expectedOptOutApps: Int? = null private var expectedOptOutApps: Int? = null
private var expectedPmRecords: Int? = null private var expectedPmRecords: Int? = null
private var expectedAppTotals: ExpectedAppTotals? = null
private fun getObserverChannel(): NotificationChannel { private fun getObserverChannel(): NotificationChannel {
val title = context.getString(R.string.notification_channel_title) val title = context.getString(R.string.notification_channel_title)
@ -67,17 +71,18 @@ class BackupNotificationManager(private val context: Context) {
*/ */
fun onBackupStarted( fun onBackupStarted(
expectedPackages: Int, expectedPackages: Int,
expectedOptOutPackages: Int, appTotals: ExpectedAppTotals,
userInitiated: Boolean userInitiated: Boolean
) { ) {
updateBackupNotification( updateBackupNotification(
contentText = "", // This passes quickly, no need to show something here infoText = "", // This passes quickly, no need to show something here
transferred = 0, transferred = 0,
expected = expectedPackages, expected = expectedPackages,
userInitiated = userInitiated userInitiated = userInitiated
) )
expectedApps = expectedPackages expectedApps = expectedPackages
expectedOptOutApps = expectedOptOutPackages expectedOptOutApps = appTotals.appsOptOut
expectedAppTotals = appTotals
} }
/** /**
@ -88,11 +93,9 @@ class BackupNotificationManager(private val context: Context) {
Log.d(TAG, "Expected number of apps unknown. Not showing @pm@ notification.") Log.d(TAG, "Expected number of apps unknown. Not showing @pm@ notification.")
return return
} }
val appName = getAppName(context, packageName)
val contentText = context.getString(R.string.notification_content_package_manager, appName)
val addend = (expectedOptOutApps ?: 0) + (expectedApps ?: 0) val addend = (expectedOptOutApps ?: 0) + (expectedApps ?: 0)
updateBackupNotification( updateBackupNotification(
contentText = contentText, infoText = "@pm@ record for $packageName",
transferred = transferred, transferred = transferred,
expected = expected + addend, expected = expected + addend,
userInitiated = false userInitiated = false
@ -108,10 +111,8 @@ class BackupNotificationManager(private val context: Context) {
Log.d(TAG, "Expected number of apps unknown. Not showing APK notification.") Log.d(TAG, "Expected number of apps unknown. Not showing APK notification.")
return return
} }
val appName = getAppName(context, packageName)
val contentText = context.getString(R.string.notification_content_opt_out_app, appName)
updateBackupNotification( updateBackupNotification(
contentText = contentText, infoText = "Opt-out APK for $packageName",
transferred = transferred + (expectedPmRecords ?: 0), transferred = transferred + (expectedPmRecords ?: 0),
expected = expected + (expectedApps ?: 0) + (expectedPmRecords ?: 0), expected = expected + (expectedApps ?: 0) + (expectedPmRecords ?: 0),
userInitiated = false userInitiated = false
@ -127,7 +128,7 @@ class BackupNotificationManager(private val context: Context) {
val expected = expectedApps ?: error("expectedApps is null") val expected = expectedApps ?: error("expectedApps is null")
val addend = (expectedOptOutApps ?: 0) + (expectedPmRecords ?: 0) val addend = (expectedOptOutApps ?: 0) + (expectedPmRecords ?: 0)
updateBackupNotification( updateBackupNotification(
contentText = app, infoText = app,
transferred = transferred + addend, transferred = transferred + addend,
expected = expected + addend, expected = expected + addend,
userInitiated = userInitiated userInitiated = userInitiated
@ -135,16 +136,20 @@ class BackupNotificationManager(private val context: Context) {
} }
private fun updateBackupNotification( private fun updateBackupNotification(
contentText: CharSequence, infoText: CharSequence,
transferred: Int, transferred: Int,
expected: Int, expected: Int,
userInitiated: Boolean userInitiated: Boolean
) { ) {
Log.i(TAG, "$transferred/$expected $contentText") @Suppress("MagicNumber")
val percentage = (transferred.toFloat() / expected) * 100
val percentageStr = "%.0f%%".format(percentage)
Log.i(TAG, "$transferred/$expected - $percentageStr - $infoText")
val notification = Builder(context, CHANNEL_ID_OBSERVER).apply { val notification = Builder(context, CHANNEL_ID_OBSERVER).apply {
setSmallIcon(R.drawable.ic_cloud_upload) setSmallIcon(R.drawable.ic_cloud_upload)
setContentTitle(context.getString(R.string.notification_title)) setContentTitle(context.getString(R.string.notification_title))
setContentText(contentText) setContentText(percentageStr)
setTicker(infoText)
setOngoing(true) setOngoing(true)
setShowWhen(false) setShowWhen(false)
setWhen(System.currentTimeMillis()) setWhen(System.currentTimeMillis())
@ -154,15 +159,17 @@ class BackupNotificationManager(private val context: Context) {
nm.notify(NOTIFICATION_ID_OBSERVER, notification) nm.notify(NOTIFICATION_ID_OBSERVER, notification)
} }
fun onBackupFinished(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) { fun onBackupFinished(success: Boolean, numBackedUp: Int?, userInitiated: Boolean) {
if (!userInitiated) { if (!userInitiated) {
// don't show permanent finished notification if backup was not triggered by user
nm.cancel(NOTIFICATION_ID_OBSERVER) nm.cancel(NOTIFICATION_ID_OBSERVER)
return return
} }
val titleRes = val titleRes =
if (success) R.string.notification_success_title else R.string.notification_failed_title if (success) R.string.notification_success_title else R.string.notification_failed_title
val contentText = if (notBackedUp == null) null else { val total = expectedAppTotals?.appsTotal
context.getString(R.string.notification_success_num_not_backed_up, notBackedUp) val contentText = if (numBackedUp == null || total == null) null else {
context.getString(R.string.notification_success_text, numBackedUp, total)
} }
val iconRes = if (success) R.drawable.ic_cloud_done else R.drawable.ic_cloud_error val iconRes = if (success) R.drawable.ic_cloud_done else R.drawable.ic_cloud_error
val intent = Intent(context, SettingsActivity::class.java).apply { val intent = Intent(context, SettingsActivity::class.java).apply {
@ -186,6 +193,7 @@ class BackupNotificationManager(private val context: Context) {
expectedOptOutApps = null expectedOptOutApps = null
expectedPmRecords = null expectedPmRecords = null
expectedApps = null expectedApps = null
expectedAppTotals = null
} }
fun onBackupError() { fun onBackupError() {

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault package com.stevesoltys.seedvault.ui.notification
import android.app.backup.BackupProgress import android.app.backup.BackupProgress
import android.app.backup.IBackupObserver import android.app.backup.IBackupObserver
@ -7,16 +7,19 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.util.Log import android.util.Log
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.isLoggable import android.util.Log.isLoggable
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.transport.backup.ExpectedAppTotals
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
private val TAG = NotificationBackupObserver::class.java.simpleName private val TAG = NotificationBackupObserver::class.java.simpleName
class NotificationBackupObserver( internal class NotificationBackupObserver(
private val context: Context, private val context: Context,
private val expectedPackages: Int, private val expectedPackages: Int,
expectedOptOutPackages: Int, appTotals: ExpectedAppTotals,
private val userInitiated: Boolean private val userInitiated: Boolean
) : IBackupObserver.Stub(), KoinComponent { ) : IBackupObserver.Stub(), KoinComponent {
@ -28,7 +31,7 @@ class NotificationBackupObserver(
init { init {
// Inform the notification manager that a backup has started // Inform the notification manager that a backup has started
// and inform about the expected numbers, so it can compute a total. // and inform about the expected numbers, so it can compute a total.
nm.onBackupStarted(expectedPackages, expectedOptOutPackages, userInitiated) nm.onBackupStarted(expectedPackages, appTotals, userInitiated)
} }
/** /**
@ -75,8 +78,8 @@ class NotificationBackupObserver(
Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status") Log.i(TAG, "Backup finished $numPackages/$expectedPackages. Status: $status")
} }
val success = status == 0 val success = status == 0
val notBackedUp = if (success) metadataManager.getPackagesNumNotBackedUp() else null val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null
nm.onBackupFinished(success, notBackedUp, userInitiated) nm.onBackupFinished(success, numBackedUp, userInitiated)
} }
private fun showProgressNotification(packageName: String) { private fun showProgressNotification(packageName: String) {

View file

@ -73,16 +73,12 @@
<!-- Notification --> <!-- Notification -->
<string name="notification_channel_title">Backup notification</string> <string name="notification_channel_title">Backup notification</string>
<string name="notification_title">Backup running</string> <string name="notification_title">Backup running</string>
<!-- This is shown in a backup notification when metadata for an app is being backed up -->
<string name="notification_content_package_manager">Metadata for %s</string>
<!-- This is shown in a backup notification when *only* the APK of an app that opts out of backup gets backed up -->
<string name="notification_content_opt_out_app">Only app %s</string>
<string name="notification_backup_result_complete">Backup complete</string> <string name="notification_backup_result_complete">Backup complete</string>
<string name="notification_backup_result_rejected">Not backed up</string> <string name="notification_backup_result_rejected">Not backed up</string>
<string name="notification_backup_result_error">Backup failed</string> <string name="notification_backup_result_error">Backup failed</string>
<string name="notification_success_title">Backup finished</string> <string name="notification_success_title">Backup finished</string>
<string name="notification_success_num_not_backed_up">%1$d apps could not get backed up</string> <string name="notification_success_text">%1$d of %2$d apps backed up. Tap to learn more.</string>
<string name="notification_failed_title">Backup failed</string> <string name="notification_failed_title">Backup failed</string>
<string name="notification_error_channel_title">Error notification</string> <string name="notification_error_channel_title">Error notification</string>
@ -108,6 +104,8 @@
<string name="restore_next">Next</string> <string name="restore_next">Next</string>
<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 -->
<string name="restore_app_not_eligible">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

@ -7,7 +7,7 @@ import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription
import android.app.backup.RestoreDescription.TYPE_FULL_STREAM import android.app.backup.RestoreDescription.TYPE_FULL_STREAM
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.crypto.CipherFactoryImpl import com.stevesoltys.seedvault.crypto.CipherFactoryImpl
import com.stevesoltys.seedvault.crypto.CryptoImpl import com.stevesoltys.seedvault.crypto.CryptoImpl
import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl

View file

@ -8,7 +8,7 @@ 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.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString

View file

@ -7,7 +7,7 @@ import android.app.backup.BackupTransport.TRANSPORT_ERROR
import android.app.backup.BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED import android.app.backup.BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED
import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.BackupTransport.TRANSPORT_OK
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import com.stevesoltys.seedvault.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.Utf8 import com.stevesoltys.seedvault.Utf8
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE

View file

@ -9,7 +9,7 @@ import android.app.backup.RestoreDescription.TYPE_KEY_VALUE
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
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.ui.notification.BackupNotificationManager
import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.coAssertThrows
import com.stevesoltys.seedvault.getRandomString import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.BackupMetadata import com.stevesoltys.seedvault.metadata.BackupMetadata