From 80187c8c700eaf1d37be0872c6bcfad18c4b8f9a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 28 Aug 2020 15:22:17 -0300 Subject: [PATCH] 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. --- .../java/com/stevesoltys/seedvault/App.kt | 3 +- .../seedvault/metadata/MetadataManager.kt | 8 +- .../restore/RestoreErrorBroadcastReceiver.kt | 2 +- .../restore/RestoreProgressAdapter.kt | 5 +- .../seedvault/restore/RestoreViewModel.kt | 2 +- .../seedvault/settings/SettingsActivity.kt | 2 +- .../seedvault/settings/SettingsViewModel.kt | 76 +++++++++---------- .../ConfigurableBackupTransportService.kt | 8 +- .../transport/backup/BackupCoordinator.kt | 2 +- .../seedvault/transport/backup/KVBackup.kt | 2 +- .../transport/backup/PackageService.kt | 57 ++++++++++++-- .../transport/restore/RestoreCoordinator.kt | 2 +- .../stevesoltys/seedvault/ui/AppViewHolder.kt | 2 + .../BackupNotificationManager.kt | 44 ++++++----- .../NotificationBackupObserver.kt | 15 ++-- app/src/main/res/values/strings.xml | 8 +- .../transport/CoordinatorIntegrationTest.kt | 2 +- .../transport/backup/BackupCoordinatorTest.kt | 2 +- .../transport/backup/KVBackupTest.kt | 2 +- .../restore/RestoreCoordinatorTest.kt | 2 +- 20 files changed, 155 insertions(+), 91 deletions(-) rename app/src/main/java/com/stevesoltys/seedvault/{ => ui/notification}/BackupNotificationManager.kt (86%) rename app/src/main/java/com/stevesoltys/seedvault/{ => ui/notification}/NotificationBackupObserver.kt (88%) diff --git a/app/src/main/java/com/stevesoltys/seedvault/App.kt b/app/src/main/java/com/stevesoltys/seedvault/App.kt index bc4e03c2..465ba7b8 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/App.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/App.kt @@ -15,6 +15,7 @@ import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.settings.SettingsViewModel import com.stevesoltys.seedvault.transport.backup.backupModule 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.storage.BackupStorageViewModel import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel @@ -36,7 +37,7 @@ class App : Application() { single { Clock() } factory { 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 { BackupStorageViewModel(this@App, get(), get()) } viewModel { RestoreStorageViewModel(this@App, get(), get()) } diff --git a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt index 5f36fe35..03a6e079 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/metadata/MetadataManager.kt @@ -12,6 +12,7 @@ import androidx.lifecycle.distinctUntilChanged import com.stevesoltys.seedvault.Clock import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED +import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA import com.stevesoltys.seedvault.transport.backup.isSystemApp import java.io.FileNotFoundException import java.io.IOException @@ -196,9 +197,12 @@ class MetadataManager( } @Synchronized - fun getPackagesNumNotBackedUp(): Int { + fun getPackagesNumBackedUp(): Int { 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() } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt index 4d24c841..94beee12 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreErrorBroadcastReceiver.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import androidx.core.net.toUri -import com.stevesoltys.seedvault.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import org.koin.core.context.GlobalContext.get internal const val ACTION_RESTORE_ERROR_UNINSTALL = "com.stevesoltys.seedvault.action.UNINSTALL" diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt index b22dd354..d1b0bd33 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreProgressAdapter.kt @@ -10,7 +10,7 @@ import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.restore.RestoreProgressAdapter.PackageViewHolder import com.stevesoltys.seedvault.ui.AppViewHolder -import java.util.* +import java.util.LinkedList internal class RestoreProgressAdapter : Adapter() { @@ -50,7 +50,7 @@ internal class RestoreProgressAdapter : Adapter() { } } - inner class PackageViewHolder(v: View) : AppViewHolder(v) { + class PackageViewHolder(v: View) : AppViewHolder(v) { fun bind(item: AppRestoreResult) { appName.text = item.name if (item.packageName == MAGIC_PACKAGE_MANAGER) { @@ -71,6 +71,7 @@ internal class RestoreProgressAdapter : Adapter() { enum class AppRestoreStatus { IN_PROGRESS, SUCCEEDED, + NOT_ELIGIBLE, FAILED, FAILED_NO_DATA, FAILED_NOT_ALLOWED, diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt index ec688cb9..9ca7e645 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -19,7 +19,7 @@ import com.stevesoltys.seedvault.BackupMonitor import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R 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.NOT_ALLOWED import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt index d4eacb38..064493bc 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsActivity.kt @@ -5,7 +5,7 @@ import androidx.annotation.CallSuper import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat 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.ui.RequireProvisioningActivity import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt index 2208e8cf..02c30622 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/SettingsViewModel.kt @@ -14,7 +14,6 @@ import androidx.recyclerview.widget.DiffUtil.calculateDiff import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.crypto.KeyManager -import com.stevesoltys.seedvault.getAppName import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA 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_NO_DATA 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.transport.backup.isSystemApp +import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.transport.requestBackup import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel +import com.stevesoltys.seedvault.ui.notification.getAppName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.Locale private val TAG = SettingsViewModel::class.java.simpleName -class SettingsViewModel( +internal class SettingsViewModel( app: Application, settingsManager: SettingsManager, keyManager: KeyManager, - private val metadataManager: MetadataManager + private val metadataManager: MetadataManager, + private val packageService: PackageService ) : RequireProvisioningViewModel(app, settingsManager, keyManager) { override val isRestoreOperation = false @@ -69,43 +71,41 @@ class SettingsViewModel( private fun getAppStatusResult(): LiveData = liveData { val pm = app.packageManager val locale = Locale.getDefault() - val list = pm.getInstalledPackages(0) - .filter { !it.isSystemApp() } - .map { - val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) { + val list = packageService.userApps.map { + val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) { + getDrawable(app, R.drawable.ic_launcher_default)!! + } else { + try { + pm.getApplicationIcon(it.packageName) + } catch (e: NameNotFoundException) { 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 status = when (metadata?.state) { - null -> { - Log.w(TAG, "No metadata available for: ${it.packageName}") - FAILED - } - NO_DATA -> FAILED_NO_DATA - NOT_ALLOWED -> FAILED_NOT_ALLOWED - QUOTA_EXCEEDED -> FAILED_QUOTA_EXCEEDED - UNKNOWN_ERROR -> FAILED - APK_AND_DATA -> SUCCEEDED + } + val metadata = metadataManager.getPackageMetadata(it.packageName) + val time = metadata?.time ?: 0 + val status = when (metadata?.state) { + null -> { + Log.w(TAG, "No metadata available for: ${it.packageName}") + NOT_ELIGIBLE } - if (metadata?.hasApk() == false) { - Log.w(TAG, "No APK stored for: ${it.packageName}") - } - AppStatus( - packageName = it.packageName, - enabled = settingsManager.isBackupEnabled(it.packageName), - icon = icon, - name = getAppName(app, it.packageName).toString(), - time = time, - status = status - ) - }.sortedBy { it.name.toLowerCase(locale) } + 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) { + Log.w(TAG, "No APK stored for: ${it.packageName}") + } + AppStatus( + packageName = it.packageName, + 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 diff = calculateDiff(AppStatusDiff(oldList, list)) emit(AppStatusResult(list, diff)) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt index f138b82e..d5f10b6b 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/ConfigurableBackupTransportService.kt @@ -13,8 +13,8 @@ import android.os.RemoteException import android.util.Log import androidx.annotation.WorkerThread import com.stevesoltys.seedvault.BackupMonitor -import com.stevesoltys.seedvault.BackupNotificationManager -import com.stevesoltys.seedvault.NotificationBackupObserver +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.NotificationBackupObserver import com.stevesoltys.seedvault.transport.backup.PackageService import org.koin.core.context.GlobalContext.get @@ -53,9 +53,9 @@ class ConfigurableBackupTransportService : Service() { fun requestBackup(context: Context) { val packageService: PackageService = get().koin.get() 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 backupManager: IBackupManager = get().koin.get() backupManager.requestBackup(packages, observer, BackupMonitor(), FLAG_USER_INITIATED) diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt index 0bf2b6d5..12f40c98 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinator.kt @@ -10,7 +10,7 @@ import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.os.ParcelFileDescriptor import android.util.Log 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.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.metadata.MetadataManager diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt index dee9a6b5..f02af892 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/KVBackup.kt @@ -8,7 +8,7 @@ import android.app.backup.BackupTransport.TRANSPORT_OK import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor 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.crypto.Crypto import com.stevesoltys.seedvault.encodeBase64 diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt index 42e8d85b..db5eeac1 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/backup/PackageService.kt @@ -2,16 +2,19 @@ package com.stevesoltys.seedvault.transport.backup import android.app.backup.IBackupManager 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_UPDATED_SYSTEM_APP import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.content.pm.PackageManager.GET_INSTRUMENTATION import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES import android.os.RemoteException import android.os.UserHandle import android.util.Log import android.util.Log.INFO import androidx.annotation.WorkerThread +import com.stevesoltys.seedvault.BuildConfig.APPLICATION_ID import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER 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. return packageManager.getInstalledPackages(GET_SIGNING_CERTIFICATES) .filter { packageInfo -> - !packageInfo.isBackupAllowed() && // only apps that do not allow backup - !packageInfo.isNotUpdatedSystemApp() // and are not vanilla system apps + packageInfo.doesNotGetBackedUp() && // only apps that do not allow backup + !packageInfo.isNotUpdatedSystemApp() && // and are not vanilla system apps + packageInfo.packageName != APPLICATION_ID // not this app }.sortedBy { packageInfo -> packageInfo.packageName }.also { notAllowed -> @@ -81,6 +85,32 @@ internal class PackageService( } } + /** + * A list of non-system apps (without instrumentation test apps). + */ + val userApps: List + @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) { packages.chunked(LOG_MAX_PACKAGES).forEach { 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 { if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true return applicationInfo.flags and FLAG_SYSTEM != 0 @@ -105,7 +151,8 @@ internal fun PackageInfo.isNotUpdatedSystemApp(): Boolean { return isSystemApp && !isUpdatedSystemApp } -internal fun PackageInfo.isBackupAllowed(): Boolean { - if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false - return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0 +internal fun PackageInfo.doesNotGetBackedUp(): Boolean { + if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true + return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 && // does not allow backup + applicationInfo.flags and FLAG_STOPPED != 0 // is stopped } diff --git a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt index f6f0efe6..d0cb8363 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinator.kt @@ -13,7 +13,7 @@ import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor import android.util.Log 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.R import com.stevesoltys.seedvault.header.UnsupportedVersionException diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt index 5c633c85..c9cabfe0 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/AppViewHolder.kt @@ -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_QUOTA_EXCEEDED import com.stevesoltys.seedvault.restore.AppRestoreStatus.IN_PROGRESS +import com.stevesoltys.seedvault.restore.AppRestoreStatus.NOT_ELIGIBLE 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) { + NOT_ELIGIBLE -> context.getString(R.string.restore_app_not_eligible) FAILED_NO_DATA -> context.getString(R.string.restore_app_no_data) FAILED_NOT_ALLOWED -> context.getString(R.string.restore_app_not_allowed) FAILED_NOT_INSTALLED -> context.getString(R.string.restore_app_not_installed) diff --git a/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt similarity index 86% rename from app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt rename to app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt index 8246b372..c98a0171 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/BackupNotificationManager.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/BackupNotificationManager.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.seedvault +package com.stevesoltys.seedvault.ui.notification import android.app.NotificationChannel 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_HIGH 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.EXTRA_PACKAGE_NAME import com.stevesoltys.seedvault.restore.REQUEST_CODE_UNINSTALL import com.stevesoltys.seedvault.settings.ACTION_APP_STATUS_LIST 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_ERROR = "NotificationError" @@ -31,7 +34,7 @@ private const val NOTIFICATION_ID_RESTORE_ERROR = 3 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 { createNotificationChannel(getObserverChannel()) @@ -41,6 +44,7 @@ class BackupNotificationManager(private val context: Context) { private var expectedApps: Int? = null private var expectedOptOutApps: Int? = null private var expectedPmRecords: Int? = null + private var expectedAppTotals: ExpectedAppTotals? = null private fun getObserverChannel(): NotificationChannel { val title = context.getString(R.string.notification_channel_title) @@ -67,17 +71,18 @@ class BackupNotificationManager(private val context: Context) { */ fun onBackupStarted( expectedPackages: Int, - expectedOptOutPackages: Int, + appTotals: ExpectedAppTotals, userInitiated: Boolean ) { updateBackupNotification( - contentText = "", // This passes quickly, no need to show something here + infoText = "", // This passes quickly, no need to show something here transferred = 0, expected = expectedPackages, userInitiated = userInitiated ) 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.") return } - val appName = getAppName(context, packageName) - val contentText = context.getString(R.string.notification_content_package_manager, appName) val addend = (expectedOptOutApps ?: 0) + (expectedApps ?: 0) updateBackupNotification( - contentText = contentText, + infoText = "@pm@ record for $packageName", transferred = transferred, expected = expected + addend, userInitiated = false @@ -108,10 +111,8 @@ class BackupNotificationManager(private val context: Context) { Log.d(TAG, "Expected number of apps unknown. Not showing APK notification.") return } - val appName = getAppName(context, packageName) - val contentText = context.getString(R.string.notification_content_opt_out_app, appName) updateBackupNotification( - contentText = contentText, + infoText = "Opt-out APK for $packageName", transferred = transferred + (expectedPmRecords ?: 0), expected = expected + (expectedApps ?: 0) + (expectedPmRecords ?: 0), userInitiated = false @@ -127,7 +128,7 @@ class BackupNotificationManager(private val context: Context) { val expected = expectedApps ?: error("expectedApps is null") val addend = (expectedOptOutApps ?: 0) + (expectedPmRecords ?: 0) updateBackupNotification( - contentText = app, + infoText = app, transferred = transferred + addend, expected = expected + addend, userInitiated = userInitiated @@ -135,16 +136,20 @@ class BackupNotificationManager(private val context: Context) { } private fun updateBackupNotification( - contentText: CharSequence, + infoText: CharSequence, transferred: Int, expected: Int, 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 { setSmallIcon(R.drawable.ic_cloud_upload) setContentTitle(context.getString(R.string.notification_title)) - setContentText(contentText) + setContentText(percentageStr) + setTicker(infoText) setOngoing(true) setShowWhen(false) setWhen(System.currentTimeMillis()) @@ -154,15 +159,17 @@ class BackupNotificationManager(private val context: Context) { nm.notify(NOTIFICATION_ID_OBSERVER, notification) } - fun onBackupFinished(success: Boolean, notBackedUp: Int?, userInitiated: Boolean) { + fun onBackupFinished(success: Boolean, numBackedUp: Int?, userInitiated: Boolean) { if (!userInitiated) { + // don't show permanent finished notification if backup was not triggered by user nm.cancel(NOTIFICATION_ID_OBSERVER) return } val titleRes = if (success) R.string.notification_success_title else R.string.notification_failed_title - val contentText = if (notBackedUp == null) null else { - context.getString(R.string.notification_success_num_not_backed_up, notBackedUp) + val total = expectedAppTotals?.appsTotal + 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 intent = Intent(context, SettingsActivity::class.java).apply { @@ -186,6 +193,7 @@ class BackupNotificationManager(private val context: Context) { expectedOptOutApps = null expectedPmRecords = null expectedApps = null + expectedAppTotals = null } fun onBackupError() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/NotificationBackupObserver.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt similarity index 88% rename from app/src/main/java/com/stevesoltys/seedvault/NotificationBackupObserver.kt rename to app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt index 386f29d2..4a22c063 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/NotificationBackupObserver.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/notification/NotificationBackupObserver.kt @@ -1,4 +1,4 @@ -package com.stevesoltys.seedvault +package com.stevesoltys.seedvault.ui.notification import android.app.backup.BackupProgress import android.app.backup.IBackupObserver @@ -7,16 +7,19 @@ import android.content.pm.PackageManager.NameNotFoundException import android.util.Log import android.util.Log.INFO 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.transport.backup.ExpectedAppTotals import org.koin.core.KoinComponent import org.koin.core.inject private val TAG = NotificationBackupObserver::class.java.simpleName -class NotificationBackupObserver( +internal class NotificationBackupObserver( private val context: Context, private val expectedPackages: Int, - expectedOptOutPackages: Int, + appTotals: ExpectedAppTotals, private val userInitiated: Boolean ) : IBackupObserver.Stub(), KoinComponent { @@ -28,7 +31,7 @@ class NotificationBackupObserver( init { // Inform the notification manager that a backup has started // 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") } val success = status == 0 - val notBackedUp = if (success) metadataManager.getPackagesNumNotBackedUp() else null - nm.onBackupFinished(success, notBackedUp, userInitiated) + val numBackedUp = if (success) metadataManager.getPackagesNumBackedUp() else null + nm.onBackupFinished(success, numBackedUp, userInitiated) } private fun showProgressNotification(packageName: String) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a5989440..338053ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,16 +73,12 @@ Backup notification Backup running - - Metadata for %s - - Only app %s Backup complete Not backed up Backup failed Backup finished - %1$d apps could not get backed up + %1$d of %2$d apps backed up. Tap to learn more. Backup failed Error notification @@ -108,6 +104,8 @@ Next Restoring backup System package manager + + Not yet backed up App reported no data for backup App doesn\'t allow backup App not installed diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt index 0646e9b6..6d4a8fbb 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/CoordinatorIntegrationTest.kt @@ -7,7 +7,7 @@ import android.app.backup.BackupTransport.TRANSPORT_OK import android.app.backup.RestoreDescription import android.app.backup.RestoreDescription.TYPE_FULL_STREAM 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.CryptoImpl import com.stevesoltys.seedvault.crypto.KeyManagerTestImpl diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt index ac23611f..6bbe5c0f 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/BackupCoordinatorTest.kt @@ -8,7 +8,7 @@ import android.content.pm.PackageInfo import android.net.Uri import android.os.ParcelFileDescriptor import androidx.documentfile.provider.DocumentFile -import com.stevesoltys.seedvault.BackupNotificationManager +import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER import com.stevesoltys.seedvault.coAssertThrows import com.stevesoltys.seedvault.getRandomString diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt index dbeb010d..6e44a73b 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/backup/KVBackupTest.kt @@ -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_OK 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.getRandomString import com.stevesoltys.seedvault.header.MAX_KEY_LENGTH_SIZE diff --git a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt index 7ce78cf5..6734e020 100644 --- a/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt +++ b/app/src/test/java/com/stevesoltys/seedvault/transport/restore/RestoreCoordinatorTest.kt @@ -9,7 +9,7 @@ import android.app.backup.RestoreDescription.TYPE_KEY_VALUE import android.content.pm.PackageInfo import android.os.ParcelFileDescriptor 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.getRandomString import com.stevesoltys.seedvault.metadata.BackupMetadata