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