From b1a0c1b2e2da8a1b0f109d3345416cfbfc3c2239 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 18 Nov 2020 16:33:58 -0300 Subject: [PATCH] Group app status list into three sections * important system apps * user apps * apps not allowing backup --- .../transport/backup/PackageServiceTest.kt | 2 +- .../seedvault/settings/AppListRetriever.kt | 37 +++++++- .../seedvault/settings/AppStatusAdapter.kt | 86 ++++++++++++------- .../transport/backup/BackupCoordinator.kt | 10 +-- .../transport/backup/PackageService.kt | 24 ++++-- .../layout/list_item_app_section_title.xml | 8 ++ .../main/res/layout/list_item_app_status.xml | 13 +-- app/src/main/res/values/strings.xml | 7 +- .../transport/backup/BackupCoordinatorTest.kt | 10 +-- 9 files changed, 141 insertions(+), 56 deletions(-) create mode 100644 app/src/main/res/layout/list_item_app_section_title.xml diff --git a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt index fe0ad472..4d5ecc21 100644 --- a/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt +++ b/app/src/androidTest/java/com/stevesoltys/seedvault/transport/backup/PackageServiceTest.kt @@ -14,7 +14,7 @@ class PackageServiceTest : KoinComponent { @Test fun testNotAllowedPackages() { - val packages = packageService.notAllowedPackages + val packages = packageService.notBackedUpPackages Log.e("TEST", "Packages: $packages") } diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt index 00783c3c..5f8f0025 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppListRetriever.kt @@ -1,5 +1,6 @@ package com.stevesoltys.seedvault.settings +import android.annotation.StringRes import android.content.Context import android.content.pm.PackageManager import android.graphics.drawable.Drawable @@ -28,6 +29,20 @@ private const val PACKAGE_NAME_SETTINGS = "com.android.providers.settings" private const val PACKAGE_NAME_CALL_LOG = "com.android.calllogbackup" private const val PACKAGE_NAME_CONTACTS = "org.calyxos.backup.contacts" +sealed class AppListItem + +data class AppStatus( + val packageName: String, + var enabled: Boolean, + val icon: Drawable, + val name: String, + val time: Long, + val status: AppBackupState, + val isSpecial: Boolean = false +) : AppListItem() + +class AppSectionTitle(@StringRes val titleRes: Int) : AppListItem() + internal class AppListRetriever( private val context: Context, private val packageService: PackageService, @@ -38,11 +53,13 @@ internal class AppListRetriever( private val pm: PackageManager = context.packageManager @WorkerThread - fun getAppList(): List { - return getSpecialApps() + getUserApps() + fun getAppList(): List { + return listOf(AppSectionTitle(R.string.backup_section_system)) + getSpecialApps() + + listOf(AppSectionTitle(R.string.backup_section_user)) + getUserApps() + + listOf(AppSectionTitle(R.string.backup_section_not_allowed)) + getNotAllowedApps() } - private fun getSpecialApps(): List { + private fun getSpecialApps(): List { val specialPackages = listOf( Pair(PACKAGE_NAME_SMS, R.string.backup_sms), Pair(PACKAGE_NAME_SETTINGS, R.string.backup_settings), @@ -86,6 +103,20 @@ internal class AppListRetriever( }.sortedBy { it.name.toLowerCase(locale) } } + private fun getNotAllowedApps(): List { + val locale = Locale.getDefault() + return packageService.userNotAllowedApps.map { + AppStatus( + packageName = it.packageName, + enabled = settingsManager.isBackupEnabled(it.packageName), + icon = getIcon(it.packageName), + name = getAppName(context, it.packageName).toString(), + time = 0, + status = FAILED_NOT_ALLOWED + ) + }.sortedBy { it.name.toLowerCase(locale) } + } + private fun getIcon(packageName: String): Drawable = when (packageName) { MAGIC_PACKAGE_MANAGER -> context.getDrawable(R.drawable.ic_launcher_default)!! PACKAGE_NAME_SMS -> context.getDrawable(R.drawable.ic_message)!! diff --git a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt index 2f6336ce..11dc53ef 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/settings/AppStatusAdapter.kt @@ -1,7 +1,6 @@ package com.stevesoltys.seedvault.settings import android.content.Intent -import android.graphics.drawable.Drawable import android.net.Uri import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS import android.view.LayoutInflater @@ -11,34 +10,53 @@ import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageView.ScaleType +import android.widget.TextView import androidx.core.content.ContextCompat.startActivity import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil.DiffResult +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.NO_POSITION import com.stevesoltys.seedvault.R -import com.stevesoltys.seedvault.settings.AppStatusAdapter.AppStatusViewHolder -import com.stevesoltys.seedvault.ui.AppBackupState +import com.stevesoltys.seedvault.ui.AppBackupState.FAILED_NOT_ALLOWED import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED import com.stevesoltys.seedvault.ui.AppViewHolder import com.stevesoltys.seedvault.ui.toRelativeTime internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) : - Adapter() { + Adapter() { - private val items = ArrayList() + private val items = ArrayList() private var editMode = false - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder { - val v = LayoutInflater.from(parent.context) - .inflate(R.layout.list_item_app_status, parent, false) - return AppStatusViewHolder(v) + override fun getItemViewType(position: Int): Int = when (items[position]) { + is AppStatus -> 0 + is AppSectionTitle -> 1 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + 0 -> { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_app_status, parent, false) + AppStatusViewHolder(v) + } + 1 -> { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_app_section_title, parent, false) + AppSectionTitleViewHolder(v) + } + else -> throw AssertionError("unknown view type") + } } override fun getItemCount() = items.size - override fun onBindViewHolder(holder: AppStatusViewHolder, position: Int) { - holder.bind(items[position]) + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is AppStatusViewHolder -> holder.bind(items[position] as AppStatus) + is AppSectionTitleViewHolder -> holder.bind(items[position] as AppSectionTitle) + } } fun setEditMode(enabled: Boolean) { @@ -46,17 +64,24 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe notifyDataSetChanged() } - fun update(newItems: List, diff: DiffResult) { + fun update(newItems: List, diff: DiffResult) { items.clear() items.addAll(newItems) diff.dispatchUpdatesTo(this) } fun onItemChanged(item: AppStatus) { - val pos = items.indexOfFirst { it.packageName == item.packageName } + val pos = items.indexOfFirst { it is AppStatus && it.packageName == item.packageName } if (pos != NO_POSITION) notifyItemChanged(pos, item) } + class AppSectionTitleViewHolder(v: View) : RecyclerView.ViewHolder(v) { + private val titleView: TextView = v as TextView + fun bind(item: AppSectionTitle) { + titleView.setText(item.titleRes) + } + } + inner class AppStatusViewHolder(v: View) : AppViewHolder(v) { fun bind(item: AppStatus) { appName.text = item.name @@ -83,7 +108,13 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe startActivity(context, intent, null) true } - setState(item.status, false) + if (item.status == FAILED_NOT_ALLOWED) { + appStatus.visibility = INVISIBLE + progressBar.visibility = INVISIBLE + appInfo.visibility = GONE + } else { + setState(item.status, false) + } if (item.status == SUCCEEDED) { appInfo.text = item.time.toRelativeTime(context) appInfo.visibility = VISIBLE @@ -106,34 +137,31 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe } -data class AppStatus( - val packageName: String, - var enabled: Boolean, - val icon: Drawable, - val name: String, - val time: Long, - val status: AppBackupState, - val isSpecial: Boolean = false -) - internal class AppStatusDiff( - private val oldItems: List, - private val newItems: List + private val oldItems: List, + private val newItems: List ) : DiffUtil.Callback() { override fun getOldListSize() = oldItems.size override fun getNewListSize() = newItems.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldItems[oldItemPosition].packageName == newItems[newItemPosition].packageName + val old = oldItems[oldItemPosition] + val new = newItems[newItemPosition] + if (old is AppSectionTitle && new is AppSectionTitle) return old.titleRes == new.titleRes + if (old is AppStatus && new is AppStatus) return old.packageName == new.packageName + return false } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldItems[oldItemPosition] == newItems[newItemPosition] + val old = oldItems[oldItemPosition] + val new = newItems[newItemPosition] + if (old is AppSectionTitle && new is AppSectionTitle) return old.titleRes == new.titleRes + return old == new } } internal class AppStatusResult( - val appStatusList: List, + val appStatusList: List, val diff: DiffResult ) 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 bf48b8bb..0d30472c 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 @@ -259,7 +259,7 @@ internal class BackupCoordinator( val result = kv.performBackup(packageInfo, data, flags) if (result == TRANSPORT_OK && packageName == MAGIC_PACKAGE_MANAGER) { // hook in here to back up APKs of apps that are otherwise not allowed for backup - backUpNotAllowedPackages() + backUpApksOfNotBackedUpPackages() } return result } @@ -388,13 +388,13 @@ internal class BackupCoordinator( } @VisibleForTesting(otherwise = PRIVATE) - internal suspend fun backUpNotAllowedPackages() { + internal suspend fun backUpApksOfNotBackedUpPackages() { Log.d(TAG, "Checking if APKs of opt-out apps need backup...") - val notAllowedPackages = packageService.notAllowedPackages - notAllowedPackages.forEachIndexed { i, packageInfo -> + val notBackedUpPackages = packageService.notBackedUpPackages + notBackedUpPackages.forEachIndexed { i, packageInfo -> val packageName = packageInfo.packageName try { - nm.onOptOutAppBackup(packageName, i + 1, notAllowedPackages.size) + nm.onOptOutAppBackup(packageName, i + 1, notBackedUpPackages.size) val packageState = if (packageInfo.isStopped()) WAS_STOPPED else NOT_ALLOWED val wasBackedUp = backUpApk(packageInfo, packageState) 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 28155187..3b4e6f3d 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 @@ -66,7 +66,7 @@ internal class PackageService( return packageArray.toTypedArray() } - val notAllowedPackages: List + val notBackedUpPackages: List @WorkerThread get() { // We need the GET_SIGNING_CERTIFICATES flag here, @@ -88,13 +88,22 @@ internal class PackageService( } /** - * A list of non-system apps (without instrumentation test apps). + * A list of non-system apps + * (without instrumentation test apps and without apps that don't allow backup). */ val userApps: List @WorkerThread - get() { - return packageManager.getInstalledPackages(GET_INSTRUMENTATION) - .filter { it.isUserVisible(context) } + get() = packageManager.getInstalledPackages(GET_INSTRUMENTATION).filter { packageInfo -> + packageInfo.isUserVisible(context) && packageInfo.allowsBackup() + } + + /** + * A list of apps that does not allow backup. + */ + val userNotAllowedApps: List + @WorkerThread + get() = packageManager.getInstalledPackages(0).filter { packageInfo -> + !packageInfo.allowsBackup() && !packageInfo.isSystemApp() } val expectedAppTotals: ExpectedAppTotals @@ -148,6 +157,11 @@ internal fun PackageInfo.isSystemApp(): Boolean { return applicationInfo.flags and FLAG_SYSTEM != 0 } +internal fun PackageInfo.allowsBackup(): Boolean { + if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return false + return applicationInfo.flags and FLAG_ALLOW_BACKUP != 0 +} + /** * Returns true if this is a system app that hasn't been updated. * We don't back up those APKs. diff --git a/app/src/main/res/layout/list_item_app_section_title.xml b/app/src/main/res/layout/list_item_app_section_title.xml new file mode 100644 index 00000000..c988c313 --- /dev/null +++ b/app/src/main/res/layout/list_item_app_section_title.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/list_item_app_status.xml b/app/src/main/res/layout/list_item_app_status.xml index 25801a7c..89846606 100644 --- a/app/src/main/res/layout/list_item_app_status.xml +++ b/app/src/main/res/layout/list_item_app_status.xml @@ -12,8 +12,8 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac623458..f74cc1dc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,7 +24,7 @@ Disabled app backup will still back up app data. However, it will not get restored automatically.\n\nYou will need to install all your apps manually while having \"Automatic Restore\" switched on. Cancel Disable app backup - App backup status + Backup status Last backup: %1$s Exclude apps Backup now @@ -91,21 +91,24 @@ + System Apps SMS text messages Device settings Call history Local contacts + User Apps Waiting to back up… Was not yet backed up Not backed up as it wasn\'t used recently Was not backed up as it hadn\'t been used recently App reported no data for backup - App doesn\'t allow backup + App doesn\'t allow backup App didn\'t allow backup Backup quota exceeded Backup quota was exceeded App not installed + Apps that do not allow data backup Restore from backup 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 74960721..d8375e63 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 @@ -357,7 +357,7 @@ internal class BackupCoordinatorTest : BackupTest() { // do actual @pm@ backup coEvery { kv.performBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK // now check if we have opt-out apps that we need to back up APKs for - every { packageService.notAllowedPackages } returns notAllowedPackages + every { packageService.notBackedUpPackages } returns notAllowedPackages // update notification every { notificationManager.onOptOutAppBackup( @@ -411,7 +411,7 @@ internal class BackupCoordinatorTest : BackupTest() { fun `APK backup of not allowed apps updates state even without new APK`() = runBlocking { val oldPackageMetadata: PackageMetadata = mockk() - every { packageService.notAllowedPackages } returns listOf(packageInfo) + every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1) } just Runs @@ -431,7 +431,7 @@ internal class BackupCoordinatorTest : BackupTest() { } just Runs every { metadataOutputStream.close() } just Runs - backup.backUpNotAllowedPackages() + backup.backUpApksOfNotBackedUpPackages() verify { metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream) @@ -441,7 +441,7 @@ internal class BackupCoordinatorTest : BackupTest() { @Test fun `APK backup of not allowed apps updates state even without old state`() = runBlocking { - every { packageService.notAllowedPackages } returns listOf(packageInfo) + every { packageService.notBackedUpPackages } returns listOf(packageInfo) every { notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1) } just Runs @@ -459,7 +459,7 @@ internal class BackupCoordinatorTest : BackupTest() { } just Runs every { metadataOutputStream.close() } just Runs - backup.backUpNotAllowedPackages() + backup.backUpApksOfNotBackedUpPackages() verify { metadataManager.onPackageBackupError(packageInfo, NOT_ALLOWED, metadataOutputStream)