Sort app selection like backup status and show sections
system data comes first and then apps
This commit is contained in:
parent
af1b3de9cb
commit
573e48f393
6 changed files with 129 additions and 33 deletions
|
@ -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<AppSelectionViewHolder>() {
|
||||
) : Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val diffCallback = object : ItemCallback<SelectableAppItem>() {
|
||||
private val diffCallback = object : ItemCallback<AppSelectionItem>() {
|
||||
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<SelectableAppItem>) {
|
||||
differ.submitList(items)
|
||||
fun submitList(items: List<AppSelectionItem>) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -23,5 +23,5 @@ val systemData = mapOf(
|
|||
|
||||
data class SystemData(
|
||||
@StringRes val nameRes: Int,
|
||||
@DrawableRes val iconRes: Int?,
|
||||
@DrawableRes val iconRes: Int,
|
||||
)
|
||||
|
|
12
app/src/main/res/drawable/ic_app_settings.xml
Normal file
12
app/src/main/res/drawable/ic_app_settings.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21.81,12.74l-0.82,-0.63v-0.22l0.8,-0.63c0.16,-0.12 0.2,-0.34 0.1,-0.51l-0.85,-1.48c-0.07,-0.13 -0.21,-0.2 -0.35,-0.2 -0.05,0 -0.1,0.01 -0.15,0.03l-0.95,0.38c-0.08,-0.05 -0.11,-0.07 -0.19,-0.11l-0.15,-1.01c-0.03,-0.21 -0.2,-0.36 -0.4,-0.36h-1.71c-0.2,0 -0.37,0.15 -0.4,0.34l-0.14,1.01c-0.03,0.02 -0.07,0.03 -0.1,0.05l-0.09,0.06 -0.95,-0.38c-0.05,-0.02 -0.1,-0.03 -0.15,-0.03 -0.14,0 -0.27,0.07 -0.35,0.2l-0.85,1.48c-0.1,0.17 -0.06,0.39 0.1,0.51l0.8,0.63v0.23l-0.8,0.63c-0.16,0.12 -0.2,0.34 -0.1,0.51l0.85,1.48c0.07,0.13 0.21,0.2 0.35,0.2 0.05,0 0.1,-0.01 0.15,-0.03l0.95,-0.37c0.08,0.05 0.12,0.07 0.2,0.11l0.15,1.01c0.03,0.2 0.2,0.34 0.4,0.34h1.71c0.2,0 0.37,-0.15 0.4,-0.34l0.15,-1.01c0.03,-0.02 0.07,-0.03 0.1,-0.05l0.09,-0.06 0.95,0.38c0.05,0.02 0.1,0.03 0.15,0.03 0.14,0 0.27,-0.07 0.35,-0.2l0.85,-1.48c0.1,-0.17 0.06,-0.39 -0.1,-0.51zM18,13.5c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5 1.5,0.67 1.5,1.5 -0.67,1.5 -1.5,1.5zM17,17h2v4c0,1.1 -0.9,2 -2,2H7c-1.1,0 -2,-0.9 -2,-2V3c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v4h-2V6H7v12h10v-1z" />
|
||||
|
||||
</vector>
|
|
@ -182,6 +182,7 @@
|
|||
<string name="backup_settings">Device settings</string>
|
||||
<string name="backup_call_log">Call history</string>
|
||||
<string name="backup_contacts">Local contacts</string>
|
||||
<string name="backup_system_apps">System apps</string>
|
||||
<string name="backup_section_user">Apps</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="backup_app_not_yet_backed_up">Waiting to back up…</string>
|
||||
|
|
Loading…
Reference in a new issue