diff --git a/res/layout/app_list_item.xml b/res/layout/app_list_item.xml new file mode 100644 index 0000000..bdf927a --- /dev/null +++ b/res/layout/app_list_item.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + diff --git a/res/layout/app_list_layout.xml b/res/layout/app_list_layout.xml new file mode 100644 index 0000000..d97c87c --- /dev/null +++ b/res/layout/app_list_layout.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/res/menu/app_list_menu.xml b/res/menu/app_list_menu.xml new file mode 100644 index 0000000..fd33084 --- /dev/null +++ b/res/menu/app_list_menu.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/res/values/cherish_dimens.xml b/res/values/cherish_dimens.xml index fe7f127..27f18ae 100644 --- a/res/values/cherish_dimens.xml +++ b/res/values/cherish_dimens.xml @@ -30,4 +30,7 @@ 10dp 100dp 20dp + + 16dp + 16dp diff --git a/res/values/cherish_strings.xml b/res/values/cherish_strings.xml index db6e477..317fe5c 100644 --- a/res/values/cherish_strings.xml +++ b/res/values/cherish_strings.xml @@ -110,4 +110,7 @@ Volume rocker wake + + Search + Search apps diff --git a/src/com/cherish/settings/CherishBasePreferenceController.kt b/src/com/cherish/settings/CherishBasePreferenceController.kt new file mode 100644 index 0000000..a2a7b53 --- /dev/null +++ b/src/com/cherish/settings/CherishBasePreferenceController.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 AOSP-Krypton Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cherish.settings + +import android.content.Context +import android.util.Log + +import com.android.settings.core.BasePreferenceController +import com.android.settings.core.PreferenceControllerMixin +import com.android.settingslib.search.SearchIndexableRaw + +abstract class CherishBasePreferenceController( + private val context: Context, + private val key: String, +): BasePreferenceController(context, key), + PreferenceControllerMixin { + + /** + * Updates non-indexable keys for search provider. + * + * Called by SearchIndexProvider#getNonIndexableKeys + */ + override fun updateNonIndexableKeys(keys: MutableList) { + val shouldSuppressFromSearch = !isAvailable() + || getAvailabilityStatus() == AVAILABLE_UNSEARCHABLE + if (shouldSuppressFromSearch) { + if (preferenceKey?.isBlank() == true) { + Log.w(TAG, "Skipping updateNonIndexableKeys due to empty key " + toString()) + return + } + if (keys.contains(key)) { + Log.w(TAG, "Skipping updateNonIndexableKeys, key already in list. " + toString()) + return + } + keys.add(key) + } + } + + /** + * Updates raw data for search provider. + * + * Called by SearchIndexProvider#getRawDataToIndex + */ + override open fun updateRawDataToIndex(rawData: MutableList) {} + + /** + * Updates dynamic raw data for search provider. + * + * Called by SearchIndexProvider#getDynamicRawDataToIndex + */ + override open fun updateDynamicRawDataToIndex(rawData: MutableList) {} + + companion object { + private const val TAG = "CherishBasePreferenceController" + } +} diff --git a/src/com/cherish/settings/fragments/AppListFragment.kt b/src/com/cherish/settings/fragments/AppListFragment.kt new file mode 100644 index 0000000..2a01b66 --- /dev/null +++ b/src/com/cherish/settings/fragments/AppListFragment.kt @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2021-2022 AOSP-Krypton Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cherish.settings.fragment + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.SearchView +import android.widget.TextView + +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView + +import com.android.settings.R +import com.google.android.material.appbar.AppBarLayout + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +/** + * [Fragment] that hosts a [RecyclerView] with a vertical + * list of application info. Items display an icon, name + * and package name of the application, along with a [CheckBox] + * indicating whether the item is selected or not. + */ +abstract class AppListFragment: Fragment(R.layout.app_list_layout), MenuItem.OnActionExpandListener { + + private val mutex = Mutex() + + private lateinit var fragmentScope: CoroutineScope + 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 val packageList = mutableListOf() + + 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) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + packageManager = requireContext().packageManager + packageList.addAll(packageManager.getInstalledPackages(0)) + } + + /** + * Override this function to set the title of this fragment. + */ + abstract protected fun getTitle(): Int + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().setTitle(getTitle()) + appBarLayout = requireActivity().findViewById(R.id.app_bar) + progressBar = view.findViewById(R.id.loading_progress) + adapter = AppListAdapter() + recyclerView = view.findViewById(R.id.apps_list).also { + it.layoutManager = LinearLayoutManager(context) + it.adapter = adapter + } + needsToHideProgressBar = true + refreshList() + } + + /** + * Abstract function for subclasses to override for providing + * an inital list of packages that should appear as selected. + */ + abstract protected fun getInitialCheckedList(): List + + 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) + } + val searchView = searchItem.actionView as SearchView + 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 { + mutex.withLock { + searchText = newText + } + refreshList() + } + return true + } + }) + } + + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + // To prevent a large space on tool bar. + appBarLayout.setExpanded(false /*expanded*/, false /*animate*/) + // To prevent user expanding the collapsing tool bar view. + ViewCompat.setNestedScrollingEnabled(recyclerView, 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*/) + // Allow user to expande the tool bar view. + ViewCompat.setNestedScrollingEnabled(recyclerView, 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] + */ + fun setDisplayCategory(category: Int) { + fragmentScope.launch { + mutex.withLock { + displayCategory = category + } + } + } + + /** + * 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. + */ + fun setCustomFilter(customFilter: ((packageInfo: PackageInfo) -> Boolean)) { + fragmentScope.launch { + mutex.withLock { + packageFilter = customFilter + } + } + } + + /** + * 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. + */ + fun setComparator(comparator: ((a: PackageInfo, b: PackageInfo) -> Int)) { + fragmentScope.launch { + mutex.withLock { + packageComparator = comparator + } + } + } + + /** + * Called when user selected list is updated. + * + * @param list a [List] of selected items. + */ + open protected fun onListUpdate(list: List) {} + + /** + * Called when user selected an application. + * + * @param packageName the package name of the selected app. + */ + open protected fun onAppSelected(packageName: String) {} + + /** + * Called when user deselected an application. + * + * @param packageName the package name of the deselected app. + */ + 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 + } + } + } + + private fun appInfofromPackage(packageInfo: PackageInfo): AppInfo = + AppInfo( + packageInfo.packageName, + getLabel(packageInfo), + packageInfo.applicationInfo.loadIcon(packageManager), + ) + + private fun getLabel(packageInfo: PackageInfo) = + packageInfo.applicationInfo.loadLabel(packageManager).toString() + + private inner class AppListAdapter : + ListAdapter(itemCallback) + { + private val checkedList = getInitialCheckedList().toMutableList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + AppListViewHolder(layoutInflater.inflate( + R.layout.app_list_item, parent, false)) + + 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.icon.setImageDrawable(item.icon) + holder.checkBox.setChecked(checkedList.contains(pkg)) + holder.itemView.setOnClickListener { + if (checkedList.contains(pkg)){ + checkedList.remove(pkg) + onAppDeselected(pkg) + } else { + checkedList.add(pkg) + onAppSelected(pkg) + } + notifyItemChanged(position) + onListUpdate(checkedList.toList()) + } + } + } + + 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) + } + + private data class AppInfo( + val packageName: String, + val label: String, + val icon: Drawable, + ) + + 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 + } + } +} diff --git a/src/com/cherish/settings/fragments/CherishDashboardFragment.kt b/src/com/cherish/settings/fragments/CherishDashboardFragment.kt new file mode 100644 index 0000000..0e9d52b --- /dev/null +++ b/src/com/cherish/settings/fragments/CherishDashboardFragment.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 AOSP-Krypton Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cherish.settings.fragment + +import androidx.preference.Preference + +import com.android.internal.logging.nano.MetricsProto +import com.android.settings.dashboard.DashboardFragment + +abstract class CherishDashboardFragment: DashboardFragment() { + override fun getMetricsCategory(): Int = MetricsProto.MetricsEvent.CHERISH_SETTINGS + + override fun onDisplayPreferenceDialog(preference: Preference) { + super.onDisplayPreferenceDialog(preference) + } + + companion object { + const val REQUEST_KEY = "CherishDashboardFragment#RequestKey" + } +}