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 } }