diff --git a/res/layout/hide_developer_status_layout.xml b/res/layout/hide_developer_status_layout.xml new file mode 100644 index 00000000000..7cfedf5c1b8 --- /dev/null +++ b/res/layout/hide_developer_status_layout.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/res/layout/hide_developer_status_list_item.xml b/res/layout/hide_developer_status_list_item.xml new file mode 100644 index 00000000000..d048f12a1b6 --- /dev/null +++ b/res/layout/hide_developer_status_list_item.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + diff --git a/res/menu/hide_developer_status_menu.xml b/res/menu/hide_developer_status_menu.xml new file mode 100644 index 00000000000..d0b638028a9 --- /dev/null +++ b/res/menu/hide_developer_status_menu.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/res/values/evolution_arrays.xml b/res/values/evolution_arrays.xml index a72abeadd32..f33d20562f3 100644 --- a/res/values/evolution_arrays.xml +++ b/res/values/evolution_arrays.xml @@ -89,4 +89,12 @@ 172800000 259200000 + + + + android + com.android.settings + com.android.systemui + com.android.shell + diff --git a/res/values/evolution_strings.xml b/res/values/evolution_strings.xml index 7cdaa96e6dc..34874255a2a 100644 --- a/res/values/evolution_strings.xml +++ b/res/values/evolution_strings.xml @@ -200,4 +200,8 @@ Disable mirroring confirmation + + + Hide developer status + Hide developer status from apps diff --git a/res/xml/more_security_privacy_settings.xml b/res/xml/more_security_privacy_settings.xml index 4c3d4c95963..5911e67be1a 100644 --- a/res/xml/more_security_privacy_settings.xml +++ b/res/xml/more_security_privacy_settings.xml @@ -238,6 +238,14 @@ android:fragment="com.android.settings.security.MemtagPage" settings:controller="com.android.settings.security.MemtagPagePreferenceController" /> + + + + userInfos; + + public HideDeveloperStatusPreferenceController(Context context) { + super(context, PREF_KEY); + userManager = UserManager.get(context); + userInfos = userManager.getUsers(); + for (UserInfo info: userInfos) { + hideDeveloperStatusUtils.setApps(context, info.id); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/src/com/android/settings/security/HideDeveloperStatusSettings.kt b/src/com/android/settings/security/HideDeveloperStatusSettings.kt new file mode 100644 index 00000000000..13e56539297 --- /dev/null +++ b/src/com/android/settings/security/HideDeveloperStatusSettings.kt @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2021 AOSP-Krypton Project + * (C) 2022 Nameless-AOSP Project + * (C) 2022 Paranoid Android + * + * 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.android.settings.security + +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.UserInfo +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.UserManager +import android.provider.Settings +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.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.internal.util.evolution.HideDeveloperStatusUtils + +import com.android.settings.R + +import com.google.android.material.appbar.AppBarLayout + +class HideDeveloperStatusSettings: Fragment(R.layout.hide_developer_status_layout) { + + private lateinit var activityManager: ActivityManager + private lateinit var packageManager: PackageManager + private lateinit var recyclerView: RecyclerView + private lateinit var adapter: AppListAdapter + private lateinit var packageList: List + private lateinit var userManager: UserManager + private lateinit var userInfos: List + + private val appBarLayout: AppBarLayout by lazy{ + requireActivity().findViewById(R.id.app_bar) + } + + private var searchText = "" + private var customFilter: ((PackageInfo) -> Boolean)? = null + private var comparator: ((PackageInfo, PackageInfo) -> Int)? = null + private var hideDeveloperStatusUtils: HideDeveloperStatusUtils = HideDeveloperStatusUtils() + private var showSystem = false + private var optionsMenu: Menu? = null + + override fun onStart() { + super.onStart() + updateOptionsMenu() + val host = getActivity() + if (host != null) { + host.invalidateOptionsMenu(); + } + } + + @SuppressLint("QueryPermissionsNeeded") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + requireActivity().setTitle(getTitle()) + activityManager = requireContext().getSystemService(ActivityManager::class.java) + packageManager = requireContext().packageManager + packageList = packageManager.getInstalledPackages(PackageManager.MATCH_ANY_USER) + userManager = UserManager.get(requireContext()) + userInfos = userManager.getUsers() + for (info in userInfos) { + hideDeveloperStatusUtils.setApps(requireContext(), info.id) + } + } + + private fun getTitle(): Int { + return R.string.hide_developer_status_title + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + adapter = AppListAdapter() + recyclerView = view.findViewById(R.id.apps_list).also { + it.layoutManager = LinearLayoutManager(context) + it.adapter = adapter + } + refreshList() + } + + /** + * @return an initial list of packages that should appear as selected. + */ + private fun getInitialCheckedList(): List { + val flattenedString = Settings.Secure.getString( + requireContext().contentResolver, getKey() + ) + return flattenedString?.takeIf { + it.isNotBlank() + }?.split(",")?.toList() ?: emptyList() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + val activity = getActivity() + if (activity == null) { + return; + } + optionsMenu = menu; + inflater.inflate(R.menu.hide_developer_status_menu, menu) + + menu.findItem(R.id.show_system).setVisible(showSystem) + menu.findItem(R.id.hide_system).setVisible(!showSystem) + + val searchMenuItem = menu.findItem(R.id.search) as MenuItem + searchMenuItem.setOnActionExpandListener(object: MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { + // To prevent a large space on tool bar. + appBarLayout.setExpanded(false /*expanded*/, false /*animate*/) + // To prevent user can expand 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*/) + ViewCompat.setNestedScrollingEnabled(recyclerView, true) + return true + } + }) + val searchView = searchMenuItem.actionView as SearchView + searchView.queryHint = getString(R.string.search_apps) + searchView.setOnQueryTextListener(object: SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String) = false + + override fun onQueryTextChange(newText: String): Boolean { + searchText = newText + refreshList() + return true + } + }) + + updateOptionsMenu() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + var i = item.getItemId() + if (i == R.id.show_system || i == R.id.hide_system) { + showSystem = !showSystem; + refreshList(); + } + updateOptionsMenu() + return true + } + + override fun onPrepareOptionsMenu(menu: Menu) { + updateOptionsMenu() + } + + override fun onDestroyOptionsMenu() { + optionsMenu = null; + } + + private fun updateOptionsMenu() { + if (optionsMenu == null) { + return; + } + + var menu = optionsMenu as Menu + + menu.findItem(R.id.show_system).setVisible(!showSystem) + menu.findItem(R.id.hide_system).setVisible(showSystem) + } + + /** + * Called when user selects an item. + * + * @param list a [List] of selected items. + */ + private fun onListUpdate(packageName: String, isChecked: Boolean) { + if (packageName.isBlank()) return + for (info in userInfos) { + if (isChecked) { + hideDeveloperStatusUtils.addApp(requireContext(), packageName, info.id) + } else { + hideDeveloperStatusUtils.removeApp(requireContext(), packageName, info.id) + } + } + try { + activityManager.forceStopPackage(packageName); + } catch (ignored: Exception) { + } + } + + private fun getKey(): String { + return Settings.Secure.HIDE_DEVELOPER_STATUS + } + + private fun refreshList() { + var list = packageList.filter { + if (!showSystem) { + !it.applicationInfo.isSystemApp() + && !resources.getStringArray( + R.array.hide_developer_status_hidden_apps) + .asList().contains(it.applicationInfo.packageName) + && !it.applicationInfo.packageName.contains("android.settings") + } else { + !resources.getStringArray( + R.array.hide_developer_status_hidden_apps) + .asList().contains(it.applicationInfo.packageName) + && !it.applicationInfo.packageName.contains("android.settings") + && !it.applicationInfo.isResourceOverlay() + } + }.filter { + getLabel(it).contains(searchText, true) + } + list = customFilter?.let { customFilter -> + list.filter { + customFilter(it) + } + } ?: list + list = comparator?.let { + list.sortedWith(it) + } ?: list.sortedWith { a, b -> + getLabel(a).compareTo(getLabel(b)) + } + if (::adapter.isInitialized) adapter.submitList(list.map { appInfoFromPackageInfo(it) }) + } + + private fun appInfoFromPackageInfo(packageInfo: PackageInfo) = + 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 selectedIndices = mutableSetOf() + private var initialList = getInitialCheckedList().toMutableList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + AppListViewHolder(layoutInflater.inflate( + R.layout.hide_developer_status_list_item, parent, false)) + + override fun onBindViewHolder(holder: AppListViewHolder, position: Int) { + getItem(position).let { + holder.label.text = it.label + holder.packageName.text = it.packageName + holder.icon.setImageDrawable(it.icon) + holder.itemView.setOnClickListener { + if (selectedIndices.contains(position)) { + selectedIndices.remove(position) + onListUpdate(holder.packageName.text.toString(), false) + } else { + selectedIndices.add(position) + onListUpdate(holder.packageName.text.toString(), true) + } + notifyItemChanged(position) + } + if (initialList.contains(it.packageName)) { + initialList.remove(it.packageName) + selectedIndices.add(position) + } + holder.checkBox.isChecked = selectedIndices.contains(position) + } + } + + override fun submitList(list: List?) { + initialList = getInitialCheckedList().toMutableList() + selectedIndices.clear() + super.submitList(list) + } + } + + 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 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 + } + } +}