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:
parent
983f917391
commit
80187c8c70
20 changed files with 155 additions and 91 deletions
|
@ -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> { 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()) }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<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) {
|
||||
appName.text = item.name
|
||||
if (item.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||
|
@ -71,6 +71,7 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
|||
enum class AppRestoreStatus {
|
||||
IN_PROGRESS,
|
||||
SUCCEEDED,
|
||||
NOT_ELIGIBLE,
|
||||
FAILED,
|
||||
FAILED_NO_DATA,
|
||||
FAILED_NOT_ALLOWED,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,9 +71,7 @@ class SettingsViewModel(
|
|||
private fun getAppStatusResult(): LiveData<AppStatusResult> = liveData {
|
||||
val pm = app.packageManager
|
||||
val locale = Locale.getDefault()
|
||||
val list = pm.getInstalledPackages(0)
|
||||
.filter { !it.isSystemApp() }
|
||||
.map {
|
||||
val list = packageService.userApps.map {
|
||||
val icon = if (it.packageName == MAGIC_PACKAGE_MANAGER) {
|
||||
getDrawable(app, R.drawable.ic_launcher_default)!!
|
||||
} else {
|
||||
|
@ -86,7 +86,7 @@ class SettingsViewModel(
|
|||
val status = when (metadata?.state) {
|
||||
null -> {
|
||||
Log.w(TAG, "No metadata available for: ${it.packageName}")
|
||||
FAILED
|
||||
NOT_ELIGIBLE
|
||||
}
|
||||
NO_DATA -> FAILED_NO_DATA
|
||||
NOT_ALLOWED -> FAILED_NOT_ALLOWED
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<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>) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
|
@ -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) {
|
|
@ -73,16 +73,12 @@
|
|||
<!-- Notification -->
|
||||
<string name="notification_channel_title">Backup notification</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_rejected">Not backed up</string>
|
||||
<string name="notification_backup_result_error">Backup failed</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_error_channel_title">Error notification</string>
|
||||
|
@ -108,6 +104,8 @@
|
|||
<string name="restore_next">Next</string>
|
||||
<string name="restore_restoring">Restoring backup</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_not_allowed">App doesn\'t allow backup</string>
|
||||
<string name="restore_app_not_installed">App not installed</string>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue