Cherish: improve app list fragment

* properly constrained image view and max lines of both label and package views
* use lifecycle scope inherent to fragments instead of creating new coroutine scopes
* fetch package list from pm every time refreshList is called
* early release locks when refreshing list
* properly annotate setDisplayCategory method
* keep AppListAdapter as a regular nested class instead of an inner class for reusability

Signed-off-by: jhonboy121 <alfredmathew05@gmail.com>
Signed-off-by: Hưng Phan <phandinhhungvp2001@gmail.com>
This commit is contained in:
jhonboy121
2022-04-18 01:50:24 +05:30
committed by Hưng Phan
parent 5112b06122
commit 7c02f3bf4d
2 changed files with 143 additions and 115 deletions

View File

@@ -25,11 +25,11 @@
android:id="@+id/icon" android:id="@+id/icon"
android:layout_width="48dp" android:layout_width="48dp"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginTop="4dp" android:layout_marginVertical="4dp"
android:layout_marginBottom="4dp"
android:scaleType="centerInside" android:scaleType="centerInside"
settings:layout_constraintStart_toStartOf="parent" settings:layout_constraintStart_toStartOf="parent"
settings:layout_constraintTop_toTopOf="parent" /> settings:layout_constraintTop_toTopOf="parent"
settings:layout_constraintBottom_toBottomOf="parent" />
<TextView <TextView
android:id="@+id/label" android:id="@+id/label"
@@ -38,23 +38,26 @@
android:layout_marginStart="@dimen/default_margin" android:layout_marginStart="@dimen/default_margin"
android:textAppearance="?android:attr/textAppearanceListItem" android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:maxLines="1"
settings:layout_constraintStart_toEndOf="@id/icon" settings:layout_constraintStart_toEndOf="@id/icon"
settings:layout_constraintEnd_toStartOf="@id/checkBox" settings:layout_constraintEnd_toStartOf="@id/check_box"
settings:layout_constraintTop_toTopOf="@id/icon" /> settings:layout_constraintTop_toTopOf="@id/icon"
settings:layout_constraintBottom_toTopOf="@id/package_name" />
<TextView <TextView
android:id="@+id/packageName" android:id="@+id/package_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall" android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" android:textColor="?android:attr/textColorSecondary"
android:maxLines="2"
settings:layout_constraintStart_toStartOf="@id/label" settings:layout_constraintStart_toStartOf="@id/label"
settings:layout_constraintEnd_toEndOf="@id/label" settings:layout_constraintEnd_toEndOf="@id/label"
settings:layout_constraintTop_toBottomOf="@id/label" settings:layout_constraintTop_toBottomOf="@id/label"
settings:layout_constraintBottom_toBottomOf="@id/icon" /> settings:layout_constraintBottom_toBottomOf="@id/icon" />
<CheckBox <CheckBox
android:id="@+id/checkBox" android:id="@+id/check_box"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusable="false" android:focusable="false"

View File

@@ -16,7 +16,7 @@
package com.cherish.settings.fragment package com.cherish.settings.fragment
import android.content.Context import android.annotation.IntDef
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
@@ -35,6 +35,7 @@ import android.widget.TextView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
@@ -43,8 +44,6 @@ import androidx.recyclerview.widget.RecyclerView
import com.android.settings.R import com.android.settings.R
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -57,38 +56,29 @@ import kotlinx.coroutines.withContext
* and package name of the application, along with a [CheckBox] * and package name of the application, along with a [CheckBox]
* indicating whether the item is selected or not. * indicating whether the item is selected or not.
*/ */
abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnActionExpandListener { abstract class AppListFragment : Fragment(R.layout.app_list_layout),
MenuItem.OnActionExpandListener {
private val mutex = Mutex() private val mutex = Mutex()
private lateinit var fragmentScope: CoroutineScope private lateinit var pm: PackageManager
private lateinit var progressBar: ProgressBar
private lateinit var appBarLayout: AppBarLayout
private lateinit var packageManager: PackageManager
private lateinit var recyclerView: RecyclerView
private lateinit var adapter: AppListAdapter private lateinit var adapter: AppListAdapter
private val packageList = mutableListOf<PackageInfo>() private var appBarLayout: AppBarLayout? = null
private var recyclerView: RecyclerView? = null
private var progressBar: ProgressBar? = null
private var searchText = "" private var searchText = ""
private var displayCategory: Int = CATEGORY_USER_ONLY private var displayCategory: Int = CATEGORY_USER_ONLY
private var packageFilter: ((PackageInfo) -> Boolean) = { true } private var packageFilter: (PackageInfo) -> Boolean = { true }
private var packageComparator: ((PackageInfo, PackageInfo) -> Int) = { a, b -> private var packageComparator: (PackageInfo, PackageInfo) -> Int = { first, second ->
getLabel(a).compareTo(getLabel(b)) getLabel(first).compareTo(getLabel(second))
}
private var needsToHideProgressBar = false
override fun onAttach(context: Context) {
super.onAttach(context)
fragmentScope = CoroutineScope(Dispatchers.Main)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
packageManager = requireContext().packageManager pm = requireContext().packageManager
packageList.addAll(packageManager.getInstalledPackages(0))
} }
/** /**
@@ -97,15 +87,19 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
abstract protected fun getTitle(): Int abstract protected fun getTitle(): Int
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
requireActivity().setTitle(getTitle()) val activity = requireActivity()
appBarLayout = requireActivity().findViewById(R.id.app_bar) activity.setTitle(getTitle())
appBarLayout = activity.findViewById(R.id.app_bar)
progressBar = view.findViewById(R.id.loading_progress) progressBar = view.findViewById(R.id.loading_progress)
adapter = AppListAdapter() adapter = AppListAdapter(getInitialCheckedList(), layoutInflater).apply {
recyclerView = view.findViewById<RecyclerView>(R.id.apps_list).also { setOnAppSelectListener { onAppSelected(it) }
setOnAppDeselectListener { onAppDeselected(it) }
setOnListUpdateListener { onListUpdate(it) }
}
recyclerView = view.findViewById<RecyclerView>(R.id.apps_list)?.also {
it.layoutManager = LinearLayoutManager(context) it.layoutManager = LinearLayoutManager(context)
it.adapter = adapter it.adapter = adapter
} }
needsToHideProgressBar = true
refreshList() refreshList()
} }
@@ -118,19 +112,21 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.app_list_menu, menu) inflater.inflate(R.menu.app_list_menu, menu)
val searchItem = menu.findItem(R.id.search).also { val searchItem = menu.findItem(R.id.search).also {
it.setOnActionExpandListener(this) if (appBarLayout != null) {
it.setOnActionExpandListener(this)
}
} }
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
searchView.setQueryHint(getString(R.string.search_apps)); searchView.setQueryHint(getString(R.string.search_apps))
searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(newText: String): Boolean { override fun onQueryTextChange(newText: String): Boolean {
fragmentScope.launch { lifecycleScope.launch {
mutex.withLock { mutex.withLock {
searchText = newText searchText = newText
} }
refreshList() refreshListInternal()
} }
return true return true
} }
@@ -139,34 +135,29 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
// To prevent a large space on tool bar. // To prevent a large space on tool bar.
appBarLayout.setExpanded(false /*expanded*/, false /*animate*/) appBarLayout?.setExpanded(false /*expanded*/, false /*animate*/)
// To prevent user expanding the collapsing tool bar view. // To prevent user expanding the collapsing tool bar view.
ViewCompat.setNestedScrollingEnabled(recyclerView, false) recyclerView?.let { ViewCompat.setNestedScrollingEnabled(it, false) }
return true return true
} }
override fun onMenuItemActionCollapse(item: MenuItem): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
// We keep the collapsed status after user cancel the search function. // We keep the collapsed status after user cancel the search function.
appBarLayout.setExpanded(false /*expanded*/, false /*animate*/) appBarLayout?.setExpanded(false /*expanded*/, false /*animate*/)
// Allow user to expande the tool bar view. // Allow user to expande the tool bar view.
ViewCompat.setNestedScrollingEnabled(recyclerView, true) recyclerView?.let { ViewCompat.setNestedScrollingEnabled(it, true) }
return true return true
} }
override fun onDetach() {
fragmentScope.cancel()
super.onDetach()
}
/** /**
* Set the type of apps that should be displayed in the list. * Set the type of apps that should be displayed in the list.
* Defaults to [CATEGORY_USER_ONLY]. * Defaults to [CATEGORY_USER_ONLY].
* *
* @param category one of [CATEGORY_SYSTEM_ONLY], * @param category one of [CATEGORY_SYSTEM_ONLY],
* [CATEGORY_USER_ONLY], [CATEGORY_BOTH] * [CATEGORY_USER_ONLY], [CATEGORY_BOTH]
*/ */
fun setDisplayCategory(category: Int) { fun setDisplayCategory(@Category category: Int) {
fragmentScope.launch { lifecycleScope.launch {
mutex.withLock { mutex.withLock {
displayCategory = category displayCategory = category
} }
@@ -177,10 +168,10 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
* Set a custom filter to filter out items from the list. * Set a custom filter to filter out items from the list.
* *
* @param customFilter a function that takes a [PackageInfo] and * @param customFilter a function that takes a [PackageInfo] and
* returns a [Boolean] indicating whether to show the item or not. * returns a [Boolean] indicating whether to show the item or not.
*/ */
fun setCustomFilter(customFilter: ((packageInfo: PackageInfo) -> Boolean)) { fun setCustomFilter(customFilter: (PackageInfo) -> Boolean) {
fragmentScope.launch { lifecycleScope.launch {
mutex.withLock { mutex.withLock {
packageFilter = customFilter packageFilter = customFilter
} }
@@ -188,13 +179,13 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
} }
/** /**
* Set a [Comparator] for sorting the elements in the list.. * Set a [Comparator] for sorting the elements in the list.
* *
* @param comparator a function that takes two [PackageInfo]'s and returns * @param comparator a function that takes two [PackageInfo]'s and returns
* an [Int] representing their relative priority. * an [Int] representing their relative priority.
*/ */
fun setComparator(comparator: ((a: PackageInfo, b: PackageInfo) -> Int)) { fun setComparator(comparator: (PackageInfo, PackageInfo) -> Int) {
fragmentScope.launch { lifecycleScope.launch {
mutex.withLock { mutex.withLock {
packageComparator = comparator packageComparator = comparator
} }
@@ -222,75 +213,111 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
*/ */
open protected fun onAppDeselected(packageName: String) {} open protected fun onAppDeselected(packageName: String) {}
protected fun refreshList() { fun refreshList() {
fragmentScope.launch { lifecycleScope.launch {
val list = withContext(Dispatchers.Default) { refreshListInternal()
mutex.withLock {
packageList.filter {
when (displayCategory) {
CATEGORY_SYSTEM_ONLY -> it.applicationInfo.isSystemApp()
CATEGORY_USER_ONLY -> !it.applicationInfo.isSystemApp()
else -> true
} &&
getLabel(it).contains(searchText, true) &&
packageFilter(it)
}.sortedWith(packageComparator).map { appInfofromPackage(it) }
}
}
adapter.submitList(list)
if (needsToHideProgressBar) {
progressBar.visibility = View.GONE
needsToHideProgressBar = false
}
} }
} }
private fun appInfofromPackage(packageInfo: PackageInfo): AppInfo = private suspend fun refreshListInternal() {
AppInfo( val list = withContext(Dispatchers.Default) {
packageInfo.packageName, val sortedList = mutex.withLock {
getLabel(packageInfo), pm.getInstalledPackages(PackageManager.MATCH_ALL).filter {
packageInfo.applicationInfo.loadIcon(packageManager), val categoryMatches = when (displayCategory) {
) CATEGORY_SYSTEM_ONLY -> it.applicationInfo.isSystemApp()
CATEGORY_USER_ONLY -> !it.applicationInfo.isSystemApp()
else -> true
}
categoryMatches && packageFilter(it) &&
getLabel(it).contains(searchText, true)
}.sortedWith(packageComparator)
}
sortedList.map {
AppInfo(
it.packageName,
getLabel(it),
it.applicationInfo.loadIcon(pm),
)
}
}
adapter.submitList(list)
progressBar?.visibility = View.GONE
}
private fun getLabel(packageInfo: PackageInfo) = private fun getLabel(packageInfo: PackageInfo) =
packageInfo.applicationInfo.loadLabel(packageManager).toString() packageInfo.applicationInfo.loadLabel(pm).toString()
private inner class AppListAdapter : private class AppListAdapter(
ListAdapter<AppInfo, AppListViewHolder>(itemCallback) initialCheckedList: List<String>,
{ private val layoutInflater: LayoutInflater
private val checkedList = getInitialCheckedList().toMutableList() ) : ListAdapter<AppInfo, AppListViewHolder>(itemCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = private val checkedList = initialCheckedList.toMutableList()
AppListViewHolder(layoutInflater.inflate( private var appSelectListener: (String) -> Unit = {}
R.layout.app_list_item, parent, false)) private var appDeselectListener: (String) -> Unit = {}
private var listUpdateListener: (List<String>) -> Unit = {}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
) = AppListViewHolder(
layoutInflater.inflate(
R.layout.app_list_item,
parent,
false /* attachToParent */
)
)
override fun onBindViewHolder(holder: AppListViewHolder, position: Int) { override fun onBindViewHolder(holder: AppListViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)
val pkg = item.packageName holder.label.text = item.label
holder.label.setText(item.label) holder.packageName.text = item.packageName
holder.packageName.setText(pkg)
holder.icon.setImageDrawable(item.icon) holder.icon.setImageDrawable(item.icon)
holder.checkBox.setChecked(checkedList.contains(pkg)) holder.checkBox.isChecked = checkedList.contains(item.packageName)
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {
if (checkedList.contains(pkg)){ if (checkedList.contains(item.packageName)) {
checkedList.remove(pkg) checkedList.remove(item.packageName)
onAppDeselected(pkg) appDeselectListener(item.packageName)
} else { } else {
checkedList.add(pkg) checkedList.add(item.packageName)
onAppSelected(pkg) appSelectListener(item.packageName)
} }
notifyItemChanged(position) notifyItemChanged(position)
onListUpdate(checkedList.toList()) listUpdateListener(checkedList.toList())
}
}
fun setOnAppSelectListener(listener: (String) -> Unit) {
appSelectListener = listener
}
fun setOnAppDeselectListener(listener: (String) -> Unit) {
appDeselectListener = listener
}
fun setOnListUpdateListener(listener: (List<String>) -> Unit) {
listUpdateListener = listener
}
companion object {
private val itemCallback = object : DiffUtil.ItemCallback<AppInfo>() {
override fun areItemsTheSame(oldInfo: AppInfo, newInfo: AppInfo) =
oldInfo.packageName == newInfo.packageName
override fun areContentsTheSame(oldInfo: AppInfo, newInfo: AppInfo) =
oldInfo == newInfo
} }
} }
} }
private class AppListViewHolder(itemView: View) : private class AppListViewHolder(
RecyclerView.ViewHolder(itemView) { itemView: View
) : RecyclerView.ViewHolder(itemView) {
val icon: ImageView = itemView.findViewById(R.id.icon) val icon: ImageView = itemView.findViewById(R.id.icon)
val label: TextView = itemView.findViewById(R.id.label) val label: TextView = itemView.findViewById(R.id.label)
val packageName: TextView = itemView.findViewById(R.id.packageName) val packageName: TextView = itemView.findViewById(R.id.package_name)
val checkBox: CheckBox = itemView.findViewById(R.id.checkBox) val checkBox: CheckBox = itemView.findViewById(R.id.check_box)
} }
private data class AppInfo( private data class AppInfo(
@@ -300,18 +327,16 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
) )
companion object { companion object {
private const val TAG = "AppListFragment"
const val CATEGORY_SYSTEM_ONLY = 0 const val CATEGORY_SYSTEM_ONLY = 0
const val CATEGORY_USER_ONLY = 1 const val CATEGORY_USER_ONLY = 1
const val CATEGORY_BOTH = 2 const val CATEGORY_BOTH = 2
private val itemCallback = object : DiffUtil.ItemCallback<AppInfo>() { @IntDef(value = intArrayOf(
override fun areItemsTheSame(oldInfo: AppInfo, newInfo: AppInfo) = CATEGORY_SYSTEM_ONLY,
oldInfo.packageName == newInfo.packageName CATEGORY_USER_ONLY,
CATEGORY_BOTH
override fun areContentsTheSame(oldInfo: AppInfo, newInfo: AppInfo) = ))
oldInfo == newInfo @Retention(AnnotationRetention.SOURCE)
} annotation class Category
} }
} }