From 573e48f393d0375d85bc96f3f9032a7897be172b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 May 2024 17:16:12 -0300 Subject: [PATCH] Sort app selection like backup status and show sections system data comes first and then apps --- .../seedvault/restore/AppSelectionAdapter.kt | 110 ++++++++++++++---- .../seedvault/restore/AppSelectionFragment.kt | 4 +- .../seedvault/restore/RestoreViewModel.kt | 33 ++++-- .../stevesoltys/seedvault/ui/SystemData.kt | 2 +- app/src/main/res/drawable/ic_app_settings.xml | 12 ++ app/src/main/res/values/strings.xml | 1 + 6 files changed, 129 insertions(+), 33 deletions(-) create mode 100644 app/src/main/res/drawable/ic_app_settings.xml diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt index 52c9d934..cf9ae858 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionAdapter.kt @@ -7,44 +7,66 @@ import android.view.LayoutInflater import android.view.View import android.view.View.INVISIBLE import android.view.ViewGroup +import android.widget.ImageView.ScaleType.CENTER +import android.widget.ImageView.ScaleType.FIT_CENTER +import android.widget.TextView +import androidx.annotation.StringRes import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil.ItemCallback +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.Adapter import androidx.recyclerview.widget.RecyclerView.VISIBLE import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.metadata.PackageMetadata -import com.stevesoltys.seedvault.restore.AppSelectionAdapter.AppSelectionViewHolder import com.stevesoltys.seedvault.ui.AppViewHolder import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +sealed interface AppSelectionItem + +internal class AppSelectionSection(@StringRes val titleRes: Int) : AppSelectionItem + internal data class SelectableAppItem( val packageName: String, val metadata: PackageMetadata, val selected: Boolean, val hasIcon: Boolean? = null, -) { +) : AppSelectionItem { val name: String get() = metadata.name?.toString() ?: packageName } internal class AppSelectionAdapter( val scope: CoroutineScope, - val iconLoader: suspend (String, (Bitmap) -> Unit) -> Unit, + val iconLoader: suspend (SelectableAppItem, (Bitmap) -> Unit) -> Unit, val listener: (SelectableAppItem) -> Unit, -) : Adapter() { +) : Adapter() { - private val diffCallback = object : ItemCallback() { + private val diffCallback = object : ItemCallback() { override fun areItemsTheSame( - oldItem: SelectableAppItem, - newItem: SelectableAppItem, - ): Boolean = oldItem.packageName == newItem.packageName + oldItem: AppSelectionItem, + newItem: AppSelectionItem, + ): Boolean { + return if (oldItem is AppSelectionSection && newItem is AppSelectionSection) { + oldItem.titleRes == newItem.titleRes + } else if (oldItem is SelectableAppItem && newItem is SelectableAppItem) { + oldItem.packageName == newItem.packageName + } else { + false + } + } override fun areContentsTheSame( - old: SelectableAppItem, - new: SelectableAppItem, + old: AppSelectionItem, + new: AppSelectionItem, ): Boolean { - return old.selected == new.selected && old.hasIcon == new.hasIcon + return if (old is AppSelectionSection && new is AppSelectionSection) { + true + } else if (old is SelectableAppItem && new is SelectableAppItem) { + old.selected == new.selected && old.hasIcon == new.hasIcon + } else { + false + } } } private val differ = AsyncListDiffer(this, diffCallback) @@ -55,27 +77,67 @@ internal class AppSelectionAdapter( override fun getItemId(position: Int): Long = position.toLong() // items never get added/removed - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppSelectionViewHolder { - val v = LayoutInflater.from(parent.context) - .inflate(R.layout.list_item_app_status, parent, false) - return AppSelectionViewHolder(v) + override fun getItemViewType(position: Int): Int = when (differ.currentList[position]) { + is SelectableAppItem -> 0 + is AppSelectionSection -> 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) + SelectableAppViewHolder(v) + } + + 1 -> { + val v = LayoutInflater.from(parent.context) + .inflate(R.layout.list_item_app_section_title, parent, false) + AppSelectionSectionViewHolder(v) + } + + else -> throw AssertionError("unknown view type") + } } override fun getItemCount() = differ.currentList.size - override fun onBindViewHolder(holder: AppSelectionViewHolder, position: Int) { - holder.bind(differ.currentList[position]) + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is SelectableAppViewHolder -> { + holder.bind(differ.currentList[position] as SelectableAppItem) + } + + is AppSelectionSectionViewHolder -> { + holder.bind(differ.currentList[position] as AppSelectionSection) + } + } } - fun submitList(items: List) { - differ.submitList(items) + fun submitList(items: List) { + val itemsWithSections = items.toMutableList().apply { + val i = indexOfLast { + it as SelectableAppItem + it.metadata.system && !it.metadata.isLaunchableSystemApp + } + add(i + 1, AppSelectionSection(R.string.backup_section_user)) + add(0, AppSelectionSection(R.string.backup_section_system)) + } + differ.submitList(itemsWithSections) } - override fun onViewRecycled(holder: AppSelectionViewHolder) { - holder.iconJob?.cancel() + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + if (holder is SelectableAppViewHolder) holder.iconJob?.cancel() } - internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) { + class AppSelectionSectionViewHolder(v: View) : RecyclerView.ViewHolder(v) { + private val titleView: TextView = v as TextView + fun bind(item: AppSelectionSection) { + titleView.setText(item.titleRes) + } + } + + internal inner class SelectableAppViewHolder(v: View) : AppViewHolder(v) { var iconJob: Job? = null @@ -99,7 +161,9 @@ internal class AppSelectionAdapter( } else if (item.hasIcon) { appIcon.alpha = 0.5f iconJob = scope.launch { - iconLoader(item.packageName) { bitmap -> + iconLoader(item) { bitmap -> + val isSpecial = item.metadata.system && !item.metadata.isLaunchableSystemApp + appIcon.scaleType = if (isSpecial) CENTER else FIT_CENTER appIcon.setImageBitmap(bitmap) appIcon.alpha = 1f } diff --git a/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt index fa381f17..1cb6bf83 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/AppSelectionFragment.kt @@ -73,8 +73,8 @@ class AppSelectionFragment : Fragment() { } } - private suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) { - viewModel.loadIcon(packageName, callback) + private suspend fun loadIcon(item: SelectableAppItem, callback: (Bitmap) -> Unit) { + viewModel.loadIcon(item, callback) } } 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 4d025c9f..c93a833e 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/restore/RestoreViewModel.kt @@ -19,6 +19,8 @@ import android.os.UserHandle import android.util.Log import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData @@ -28,6 +30,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.metadata.PackageMetadata import com.stevesoltys.seedvault.metadata.PackageMetadataMap import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED @@ -62,6 +65,7 @@ import com.stevesoltys.seedvault.ui.LiveEvent import com.stevesoltys.seedvault.ui.MutableLiveEvent import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel import com.stevesoltys.seedvault.ui.notification.getAppName +import com.stevesoltys.seedvault.ui.systemData import com.stevesoltys.seedvault.worker.FILE_BACKUP_ICONS import com.stevesoltys.seedvault.worker.IconManager import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION @@ -78,6 +82,7 @@ import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_TIMESTA import org.calyxos.backup.storage.restore.RestoreService.Companion.EXTRA_USER_ID import org.calyxos.backup.storage.ui.restore.SnapshotViewModel import java.util.LinkedList +import java.util.Locale private val TAG = RestoreViewModel::class.java.simpleName @@ -183,12 +188,19 @@ internal class RestoreViewModel( // filter and sort app items for display val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) -> if (metadata.time == 0L && !metadata.hasApk()) null - else if (packageName == MAGIC_PACKAGE_MANAGER) null + else if (metadata.system && !metadata.isLaunchableSystemApp) null else SelectableAppItem(packageName, metadata, true) - }.sortedWith { i1, i2 -> - if (i1.metadata.system == i2.metadata.system) i1.name.compareTo(i2.name, true) - else i1.metadata.system.compareTo(i2.metadata.system) + }.sortedBy { + it.name.lowercase(Locale.getDefault()) + }.toMutableList() + val systemDataItems = systemData.mapNotNull { (packageName, data) -> + val metadata = restorableBackup.packageMetadataMap[packageName] + ?: return@mapNotNull null + if (metadata.time == 0L && !metadata.hasApk()) return@mapNotNull null + val name = app.getString(data.nameRes) + SelectableAppItem(packageName, metadata.copy(name = name), true, hasIcon = true) } + items.addAll(0, systemDataItems) mSelectedApps.value = SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false) // download icons @@ -205,7 +217,7 @@ internal class RestoreViewModel( } // update state, so it knows that icons have loaded val updatedItems = items.map { item -> - item.copy(hasIcon = item.packageName in packagesWithIcons) + item.copy(hasIcon = item.hasIcon ?: false || item.packageName in packagesWithIcons) } val newState = SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true) @@ -214,8 +226,15 @@ internal class RestoreViewModel( mDisplayFragment.setEvent(SELECT_APPS) } - suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) { - iconManager.loadIcon(packageName, callback) + suspend fun loadIcon(item: SelectableAppItem, callback: (Bitmap) -> Unit) { + if (item.metadata.system && !item.metadata.isLaunchableSystemApp && + item.packageName in systemData.keys + ) { + val bitmap = getDrawable(app, systemData[item.packageName]!!.iconRes)!!.toBitmap() + callback(bitmap) + } else { + iconManager.loadIcon(item.packageName, callback) + } } fun onCheckAllAppsClicked() { diff --git a/app/src/main/java/com/stevesoltys/seedvault/ui/SystemData.kt b/app/src/main/java/com/stevesoltys/seedvault/ui/SystemData.kt index 20bf8d40..a07d9925 100644 --- a/app/src/main/java/com/stevesoltys/seedvault/ui/SystemData.kt +++ b/app/src/main/java/com/stevesoltys/seedvault/ui/SystemData.kt @@ -23,5 +23,5 @@ val systemData = mapOf( data class SystemData( @StringRes val nameRes: Int, - @DrawableRes val iconRes: Int?, + @DrawableRes val iconRes: Int, ) diff --git a/app/src/main/res/drawable/ic_app_settings.xml b/app/src/main/res/drawable/ic_app_settings.xml new file mode 100644 index 00000000..be2e27ef --- /dev/null +++ b/app/src/main/res/drawable/ic_app_settings.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a40b371e..5936dc2e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,6 +182,7 @@ Device settings Call history Local contacts + System apps Apps Waiting to back up…