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
|
||||||
import android.view.View.INVISIBLE
|
import android.view.View.INVISIBLE
|
||||||
import android.view.ViewGroup
|
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.AsyncListDiffer
|
||||||
import androidx.recyclerview.widget.DiffUtil.ItemCallback
|
import androidx.recyclerview.widget.DiffUtil.ItemCallback
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import androidx.recyclerview.widget.RecyclerView.VISIBLE
|
import androidx.recyclerview.widget.RecyclerView.VISIBLE
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.restore.AppSelectionAdapter.AppSelectionViewHolder
|
|
||||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
sealed interface AppSelectionItem
|
||||||
|
|
||||||
|
internal class AppSelectionSection(@StringRes val titleRes: Int) : AppSelectionItem
|
||||||
|
|
||||||
internal data class SelectableAppItem(
|
internal data class SelectableAppItem(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val metadata: PackageMetadata,
|
val metadata: PackageMetadata,
|
||||||
val selected: Boolean,
|
val selected: Boolean,
|
||||||
val hasIcon: Boolean? = null,
|
val hasIcon: Boolean? = null,
|
||||||
) {
|
) : AppSelectionItem {
|
||||||
val name: String get() = metadata.name?.toString() ?: packageName
|
val name: String get() = metadata.name?.toString() ?: packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AppSelectionAdapter(
|
internal class AppSelectionAdapter(
|
||||||
val scope: CoroutineScope,
|
val scope: CoroutineScope,
|
||||||
val iconLoader: suspend (String, (Bitmap) -> Unit) -> Unit,
|
val iconLoader: suspend (SelectableAppItem, (Bitmap) -> Unit) -> Unit,
|
||||||
val listener: (SelectableAppItem) -> 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(
|
override fun areItemsTheSame(
|
||||||
oldItem: SelectableAppItem,
|
oldItem: AppSelectionItem,
|
||||||
newItem: SelectableAppItem,
|
newItem: AppSelectionItem,
|
||||||
): Boolean = oldItem.packageName == newItem.packageName
|
): 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(
|
override fun areContentsTheSame(
|
||||||
old: SelectableAppItem,
|
old: AppSelectionItem,
|
||||||
new: SelectableAppItem,
|
new: AppSelectionItem,
|
||||||
): Boolean {
|
): 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)
|
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 getItemId(position: Int): Long = position.toLong() // items never get added/removed
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppSelectionViewHolder {
|
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)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_app_status, parent, false)
|
.inflate(R.layout.list_item_app_status, parent, false)
|
||||||
return AppSelectionViewHolder(v)
|
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 getItemCount() = differ.currentList.size
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AppSelectionViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
holder.bind(differ.currentList[position])
|
when (holder) {
|
||||||
|
is SelectableAppViewHolder -> {
|
||||||
|
holder.bind(differ.currentList[position] as SelectableAppItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun submitList(items: List<SelectableAppItem>) {
|
is AppSelectionSectionViewHolder -> {
|
||||||
differ.submitList(items)
|
holder.bind(differ.currentList[position] as AppSelectionSection)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewRecycled(holder: AppSelectionViewHolder) {
|
fun submitList(items: List<AppSelectionItem>) {
|
||||||
holder.iconJob?.cancel()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal inner class AppSelectionViewHolder(v: View) : AppViewHolder(v) {
|
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||||
|
if (holder is SelectableAppViewHolder) holder.iconJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
var iconJob: Job? = null
|
||||||
|
|
||||||
|
@ -99,7 +161,9 @@ internal class AppSelectionAdapter(
|
||||||
} else if (item.hasIcon) {
|
} else if (item.hasIcon) {
|
||||||
appIcon.alpha = 0.5f
|
appIcon.alpha = 0.5f
|
||||||
iconJob = scope.launch {
|
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.setImageBitmap(bitmap)
|
||||||
appIcon.alpha = 1f
|
appIcon.alpha = 1f
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,8 +73,8 @@ class AppSelectionFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
|
private suspend fun loadIcon(item: SelectableAppItem, callback: (Bitmap) -> Unit) {
|
||||||
viewModel.loadIcon(packageName, callback)
|
viewModel.loadIcon(item, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import android.os.UserHandle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources.getDrawable
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
|
@ -28,6 +30,7 @@ import com.stevesoltys.seedvault.BackupMonitor
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
|
import com.stevesoltys.seedvault.metadata.PackageMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
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.MutableLiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
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.FILE_BACKUP_ICONS
|
||||||
import com.stevesoltys.seedvault.worker.IconManager
|
import com.stevesoltys.seedvault.worker.IconManager
|
||||||
import com.stevesoltys.seedvault.worker.NUM_PACKAGES_PER_TRANSACTION
|
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.restore.RestoreService.Companion.EXTRA_USER_ID
|
||||||
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
import org.calyxos.backup.storage.ui.restore.SnapshotViewModel
|
||||||
import java.util.LinkedList
|
import java.util.LinkedList
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
private val TAG = RestoreViewModel::class.java.simpleName
|
private val TAG = RestoreViewModel::class.java.simpleName
|
||||||
|
|
||||||
|
@ -183,12 +188,19 @@ internal class RestoreViewModel(
|
||||||
// filter and sort app items for display
|
// filter and sort app items for display
|
||||||
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
|
val items = restorableBackup.packageMetadataMap.mapNotNull { (packageName, metadata) ->
|
||||||
if (metadata.time == 0L && !metadata.hasApk()) null
|
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)
|
else SelectableAppItem(packageName, metadata, true)
|
||||||
}.sortedWith { i1, i2 ->
|
}.sortedBy {
|
||||||
if (i1.metadata.system == i2.metadata.system) i1.name.compareTo(i2.name, true)
|
it.name.lowercase(Locale.getDefault())
|
||||||
else i1.metadata.system.compareTo(i2.metadata.system)
|
}.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 =
|
mSelectedApps.value =
|
||||||
SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false)
|
SelectedAppsState(apps = items, allSelected = true, iconsLoaded = false)
|
||||||
// download icons
|
// download icons
|
||||||
|
@ -205,7 +217,7 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
// update state, so it knows that icons have loaded
|
// update state, so it knows that icons have loaded
|
||||||
val updatedItems = items.map { item ->
|
val updatedItems = items.map { item ->
|
||||||
item.copy(hasIcon = item.packageName in packagesWithIcons)
|
item.copy(hasIcon = item.hasIcon ?: false || item.packageName in packagesWithIcons)
|
||||||
}
|
}
|
||||||
val newState =
|
val newState =
|
||||||
SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true)
|
SelectedAppsState(updatedItems, allSelected = true, iconsLoaded = true)
|
||||||
|
@ -214,8 +226,15 @@ internal class RestoreViewModel(
|
||||||
mDisplayFragment.setEvent(SELECT_APPS)
|
mDisplayFragment.setEvent(SELECT_APPS)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadIcon(packageName: String, callback: (Bitmap) -> Unit) {
|
suspend fun loadIcon(item: SelectableAppItem, callback: (Bitmap) -> Unit) {
|
||||||
iconManager.loadIcon(packageName, callback)
|
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() {
|
fun onCheckAllAppsClicked() {
|
||||||
|
|
|
@ -23,5 +23,5 @@ val systemData = mapOf(
|
||||||
|
|
||||||
data class SystemData(
|
data class SystemData(
|
||||||
@StringRes val nameRes: Int,
|
@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_settings">Device settings</string>
|
||||||
<string name="backup_call_log">Call history</string>
|
<string name="backup_call_log">Call history</string>
|
||||||
<string name="backup_contacts">Local contacts</string>
|
<string name="backup_contacts">Local contacts</string>
|
||||||
|
<string name="backup_system_apps">System apps</string>
|
||||||
<string name="backup_section_user">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 -->
|
<!-- 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>
|
<string name="backup_app_not_yet_backed_up">Waiting to back up…</string>
|
||||||
|
|
Loading…
Reference in a new issue