diff --git a/res/layout/app_list_item.xml b/res/layout/app_list_item.xml
index bdf927a..32486df 100644
--- a/res/layout/app_list_item.xml
+++ b/res/layout/app_list_item.xml
@@ -25,11 +25,11 @@
android:id="@+id/icon"
android:layout_width="48dp"
android:layout_height="48dp"
- android:layout_marginTop="4dp"
- android:layout_marginBottom="4dp"
+ android:layout_marginVertical="4dp"
android:scaleType="centerInside"
settings:layout_constraintStart_toStartOf="parent"
- settings:layout_constraintTop_toTopOf="parent" />
+ settings:layout_constraintTop_toTopOf="parent"
+ settings:layout_constraintBottom_toBottomOf="parent" />
+ settings:layout_constraintEnd_toStartOf="@id/check_box"
+ settings:layout_constraintTop_toTopOf="@id/icon"
+ settings:layout_constraintBottom_toTopOf="@id/package_name" />
()
+ private var appBarLayout: AppBarLayout? = null
+ private var recyclerView: RecyclerView? = null
+ private var progressBar: ProgressBar? = null
private var searchText = ""
private var displayCategory: Int = CATEGORY_USER_ONLY
- private var packageFilter: ((PackageInfo) -> Boolean) = { true }
- private var packageComparator: ((PackageInfo, PackageInfo) -> Int) = { a, b ->
- getLabel(a).compareTo(getLabel(b))
- }
-
- private var needsToHideProgressBar = false
-
- override fun onAttach(context: Context) {
- super.onAttach(context)
- fragmentScope = CoroutineScope(Dispatchers.Main)
+ private var packageFilter: (PackageInfo) -> Boolean = { true }
+ private var packageComparator: (PackageInfo, PackageInfo) -> Int = { first, second ->
+ getLabel(first).compareTo(getLabel(second))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
- packageManager = requireContext().packageManager
- packageList.addAll(packageManager.getInstalledPackages(0))
+ pm = requireContext().packageManager
}
/**
@@ -97,15 +87,19 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
abstract protected fun getTitle(): Int
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- requireActivity().setTitle(getTitle())
- appBarLayout = requireActivity().findViewById(R.id.app_bar)
+ val activity = requireActivity()
+ activity.setTitle(getTitle())
+ appBarLayout = activity.findViewById(R.id.app_bar)
progressBar = view.findViewById(R.id.loading_progress)
- adapter = AppListAdapter()
- recyclerView = view.findViewById(R.id.apps_list).also {
+ adapter = AppListAdapter(getInitialCheckedList(), layoutInflater).apply {
+ setOnAppSelectListener { onAppSelected(it) }
+ setOnAppDeselectListener { onAppDeselected(it) }
+ setOnListUpdateListener { onListUpdate(it) }
+ }
+ recyclerView = view.findViewById(R.id.apps_list)?.also {
it.layoutManager = LinearLayoutManager(context)
it.adapter = adapter
}
- needsToHideProgressBar = true
refreshList()
}
@@ -118,19 +112,21 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.app_list_menu, menu)
val searchItem = menu.findItem(R.id.search).also {
- it.setOnActionExpandListener(this)
+ if (appBarLayout != null) {
+ it.setOnActionExpandListener(this)
+ }
}
val searchView = searchItem.actionView as SearchView
- searchView.setQueryHint(getString(R.string.search_apps));
- searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener {
+ searchView.setQueryHint(getString(R.string.search_apps))
+ searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String) = false
override fun onQueryTextChange(newText: String): Boolean {
- fragmentScope.launch {
+ lifecycleScope.launch {
mutex.withLock {
searchText = newText
}
- refreshList()
+ refreshListInternal()
}
return true
}
@@ -139,34 +135,29 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
// 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.
- ViewCompat.setNestedScrollingEnabled(recyclerView, false)
+ recyclerView?.let { ViewCompat.setNestedScrollingEnabled(it, false) }
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
// 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.
- ViewCompat.setNestedScrollingEnabled(recyclerView, true)
+ recyclerView?.let { ViewCompat.setNestedScrollingEnabled(it, true) }
return true
}
- override fun onDetach() {
- fragmentScope.cancel()
- super.onDetach()
- }
-
/**
* Set the type of apps that should be displayed in the list.
* Defaults to [CATEGORY_USER_ONLY].
*
* @param category one of [CATEGORY_SYSTEM_ONLY],
- * [CATEGORY_USER_ONLY], [CATEGORY_BOTH]
+ * [CATEGORY_USER_ONLY], [CATEGORY_BOTH]
*/
- fun setDisplayCategory(category: Int) {
- fragmentScope.launch {
+ fun setDisplayCategory(@Category category: Int) {
+ lifecycleScope.launch {
mutex.withLock {
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.
*
* @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)) {
- fragmentScope.launch {
+ fun setCustomFilter(customFilter: (PackageInfo) -> Boolean) {
+ lifecycleScope.launch {
mutex.withLock {
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
- * an [Int] representing their relative priority.
+ * an [Int] representing their relative priority.
*/
- fun setComparator(comparator: ((a: PackageInfo, b: PackageInfo) -> Int)) {
- fragmentScope.launch {
+ fun setComparator(comparator: (PackageInfo, PackageInfo) -> Int) {
+ lifecycleScope.launch {
mutex.withLock {
packageComparator = comparator
}
@@ -222,75 +213,111 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
*/
open protected fun onAppDeselected(packageName: String) {}
- protected fun refreshList() {
- fragmentScope.launch {
- val list = withContext(Dispatchers.Default) {
- 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
- }
+ fun refreshList() {
+ lifecycleScope.launch {
+ refreshListInternal()
}
}
- private fun appInfofromPackage(packageInfo: PackageInfo): AppInfo =
- AppInfo(
- packageInfo.packageName,
- getLabel(packageInfo),
- packageInfo.applicationInfo.loadIcon(packageManager),
- )
+ private suspend fun refreshListInternal() {
+ val list = withContext(Dispatchers.Default) {
+ val sortedList = mutex.withLock {
+ pm.getInstalledPackages(PackageManager.MATCH_ALL).filter {
+ 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) =
- packageInfo.applicationInfo.loadLabel(packageManager).toString()
+ packageInfo.applicationInfo.loadLabel(pm).toString()
- private inner class AppListAdapter :
- ListAdapter(itemCallback)
- {
- private val checkedList = getInitialCheckedList().toMutableList()
+ private class AppListAdapter(
+ initialCheckedList: List,
+ private val layoutInflater: LayoutInflater
+ ) : ListAdapter(itemCallback) {
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
- AppListViewHolder(layoutInflater.inflate(
- R.layout.app_list_item, parent, false))
+ private val checkedList = initialCheckedList.toMutableList()
+ private var appSelectListener: (String) -> Unit = {}
+ private var appDeselectListener: (String) -> Unit = {}
+ private var listUpdateListener: (List) -> 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) {
val item = getItem(position)
- val pkg = item.packageName
- holder.label.setText(item.label)
- holder.packageName.setText(pkg)
+ holder.label.text = item.label
+ holder.packageName.text = item.packageName
holder.icon.setImageDrawable(item.icon)
- holder.checkBox.setChecked(checkedList.contains(pkg))
+ holder.checkBox.isChecked = checkedList.contains(item.packageName)
holder.itemView.setOnClickListener {
- if (checkedList.contains(pkg)){
- checkedList.remove(pkg)
- onAppDeselected(pkg)
+ if (checkedList.contains(item.packageName)) {
+ checkedList.remove(item.packageName)
+ appDeselectListener(item.packageName)
} else {
- checkedList.add(pkg)
- onAppSelected(pkg)
+ checkedList.add(item.packageName)
+ appSelectListener(item.packageName)
}
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) -> Unit) {
+ listUpdateListener = listener
+ }
+
+ companion object {
+ private val itemCallback = object : DiffUtil.ItemCallback() {
+ 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) :
- RecyclerView.ViewHolder(itemView) {
+ private class AppListViewHolder(
+ itemView: View
+ ) : RecyclerView.ViewHolder(itemView) {
+
val icon: ImageView = itemView.findViewById(R.id.icon)
val label: TextView = itemView.findViewById(R.id.label)
- val packageName: TextView = itemView.findViewById(R.id.packageName)
- val checkBox: CheckBox = itemView.findViewById(R.id.checkBox)
+ val packageName: TextView = itemView.findViewById(R.id.package_name)
+ val checkBox: CheckBox = itemView.findViewById(R.id.check_box)
}
private data class AppInfo(
@@ -300,18 +327,16 @@ abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnA
)
companion object {
- private const val TAG = "AppListFragment"
-
const val CATEGORY_SYSTEM_ONLY = 0
const val CATEGORY_USER_ONLY = 1
const val CATEGORY_BOTH = 2
- private val itemCallback = object : DiffUtil.ItemCallback() {
- override fun areItemsTheSame(oldInfo: AppInfo, newInfo: AppInfo) =
- oldInfo.packageName == newInfo.packageName
-
- override fun areContentsTheSame(oldInfo: AppInfo, newInfo: AppInfo) =
- oldInfo == newInfo
- }
+ @IntDef(value = intArrayOf(
+ CATEGORY_SYSTEM_ONLY,
+ CATEGORY_USER_ONLY,
+ CATEGORY_BOTH
+ ))
+ @Retention(AnnotationRetention.SOURCE)
+ annotation class Category
}
}