OmniControl: add settings search

based on SettingsIntelligence but without the fuzz
of having a DB. We have so little settings that we
can parse the xml files on the fly

Change-Id: Ifdced163c795c7a77a4b4128bd9b24f7a08aed8a
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
new file mode 100644
index 0000000..7e42cec
--- /dev/null
+++ b/.idea/kotlinc.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinJpsPluginSettings">
+    <option name="version" value="1.8.0-release" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 050a237..5d48e17 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="DesignSurface">
     <option name="filePathToZoomLevelMap">
diff --git a/app/src/main/java/org/omnirom/control/AbstractSettingsFragment.kt b/app/src/main/java/org/omnirom/control/AbstractSettingsFragment.kt
index c806cc0..5ce7592 100644
--- a/app/src/main/java/org/omnirom/control/AbstractSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/AbstractSettingsFragment.kt
@@ -18,7 +18,10 @@
 package org.omnirom.control
 
 import android.os.Bundle
+import android.util.Log
 import androidx.preference.PreferenceFragmentCompat
+import org.omnirom.control.search.PreferenceXmlParserUtils
+import org.omnirom.control.search.PreferenceXmlParserUtils.METADATA_KEY
 import org.omnirom.omnilib.preference.SecureCheckBoxPreference
 import org.omnirom.omnilib.preference.SecureSettingSwitchPreference
 import org.omnirom.omnilib.preference.SystemCheckBoxPreference
diff --git a/app/src/main/java/org/omnirom/control/AppListFragment.kt b/app/src/main/java/org/omnirom/control/AppListFragment.kt
index c603d8f..117ae6d 100644
--- a/app/src/main/java/org/omnirom/control/AppListFragment.kt
+++ b/app/src/main/java/org/omnirom/control/AppListFragment.kt
@@ -20,6 +20,8 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.os.Bundle
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.appcompat.app.AppCompatActivity
 import androidx.preference.Preference
 import androidx.preference.PreferenceCategory
@@ -30,6 +32,12 @@
     lateinit var appManager: ApplicationManager
     var updateAppList: Boolean = false
 
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.applist_icon
+        @XmlRes
+        val XML_RES = R.xml.applist_preferences
+    }
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.applist_settings_title)
     }
@@ -39,11 +47,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.applist_icon
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.applist_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
 
         appManager = ApplicationManager(requireContext())
 
diff --git a/app/src/main/java/org/omnirom/control/BarsSettingsFragment.kt b/app/src/main/java/org/omnirom/control/BarsSettingsFragment.kt
index c69ba4d..8fd18ac 100644
--- a/app/src/main/java/org/omnirom/control/BarsSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/BarsSettingsFragment.kt
@@ -20,16 +20,24 @@
 import android.graphics.Rect
 import android.os.Bundle
 import android.util.DisplayMetrics
+import android.util.Log
 import android.view.WindowManager
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.preference.Preference
 import androidx.preference.PreferenceCategory
+import org.omnirom.control.search.PreferenceXmlParserUtils
+import org.omnirom.control.search.PreferenceXmlParserUtils.METADATA_KEY
 import kotlin.math.min
 
 
 class BarsSettingsFragment : AbstractSettingsFragment() {
-
     private val TABLET_MIN_DPS = 600
 
+    companion object {
+        @DrawableRes val ICON_RES = R.drawable.ic_bars_tile
+        @XmlRes val XML_RES = R.xml.bars_settings_preferences
+    }
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.bars_settings_title)
     }
@@ -39,11 +47,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_bars_tile
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.bars_settings_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
 
         val taskbarCategory:PreferenceCategory? = findPreference("category_taskbar")
         if (taskbarCategory != null){
@@ -53,10 +61,6 @@
         }
     }
 
-    override fun onPreferenceTreeClick(preference: Preference): Boolean {
-        return super.onPreferenceTreeClick(preference)
-    }
-
     private fun dpiFromPx(size: Int, densityDpi: Int): Float {
         val densityRatio = densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT
         return size / densityRatio
diff --git a/app/src/main/java/org/omnirom/control/BatteryLightSettingsFragment.java b/app/src/main/java/org/omnirom/control/BatteryLightSettingsFragment.java
index 962487a..a67d5c3 100644
--- a/app/src/main/java/org/omnirom/control/BatteryLightSettingsFragment.java
+++ b/app/src/main/java/org/omnirom/control/BatteryLightSettingsFragment.java
@@ -23,6 +23,9 @@
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.provider.Settings;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.XmlRes;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceGroup;
 import androidx.preference.PreferenceScreen;
@@ -60,6 +63,13 @@
     private SystemSettingSwitchPreference mOnlyFullPref;
     private SystemSettingSwitchPreference mFastBatteryLightEnabledPref;
 
+    public static final @DrawableRes int ICON_RES = R.drawable.ic_settings_leds;
+    public static final @XmlRes int XML_RES = R.xml.battery_light_settings_preferences;
+
+    public static boolean isEnabled(Context context) {
+        return context.getResources().getBoolean(com.android.internal.R.bool.config_intrusiveBatteryLed);
+    }
+
     @Override
     public String getFragmentTitle() {
         return getString(R.string.batterylight_title);
@@ -72,12 +82,12 @@
 
     @Override
     public int getFragmentIcon() {
-        return R.drawable.ic_settings_leds;
+        return ICON_RES;
     }
 
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
-        setPreferencesFromResource(R.xml.battery_light_settings_preferences, rootKey);
+        setPreferencesFromResource(XML_RES, rootKey);
 
         PreferenceScreen prefSet = getPreferenceScreen();
         ContentResolver resolver = getContext().getContentResolver();
diff --git a/app/src/main/java/org/omnirom/control/ButtonSettingsFragment.kt b/app/src/main/java/org/omnirom/control/ButtonSettingsFragment.kt
index ba60c30..da0f713 100644
--- a/app/src/main/java/org/omnirom/control/ButtonSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/ButtonSettingsFragment.kt
@@ -20,6 +20,8 @@
 import android.content.res.Resources
 import android.os.Bundle
 import android.provider.Settings
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.appcompat.app.AppCompatActivity
 import androidx.preference.Preference
 import androidx.preference.PreferenceCategory
@@ -32,6 +34,13 @@
     private val KEY_ADVANCED_REBOOT = "advanced_reboot"
     private val KEY_POWER_TORCH = "long_press_power_torch"
 
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.ic_settings_buttons
+        @XmlRes
+        val XML_RES = R.xml.button_settings_preferences
+    }
+
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.button_settings_title)
     }
@@ -41,11 +50,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_settings_buttons
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.button_settings_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
 
         val powerCategory: PreferenceCategory? = findPreference(CATEGORY_POWER)
         if (powerCategory != null) {
diff --git a/app/src/main/java/org/omnirom/control/DialerSettingsFragment.kt b/app/src/main/java/org/omnirom/control/DialerSettingsFragment.kt
index 5762e1a..159f452 100644
--- a/app/src/main/java/org/omnirom/control/DialerSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/DialerSettingsFragment.kt
@@ -17,14 +17,28 @@
  */
 package org.omnirom.control
 
+import android.content.Context
 import android.os.Bundle
 import android.provider.Settings
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.appcompat.app.AppCompatActivity
 import androidx.preference.Preference
 import androidx.preference.PreferenceFragmentCompat
 
+import org.omnirom.omnilibcore.utils.DeviceUtils
 
 class DialerSettingsFragment : AbstractSettingsFragment() {
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.ic_dialer_tile
+
+        @XmlRes
+        val XML_RES = R.xml.dialer_settings_preferences
+        fun isEnabled(context: Context): Boolean {
+            return DeviceUtils.isVoiceCapable(context)
+        }
+    }
 
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.dialer_settings_title)
@@ -35,11 +49,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_dialer_tile
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.dialer_settings_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
     }
 
     override fun onPreferenceTreeClick(preference: Preference): Boolean {
diff --git a/app/src/main/java/org/omnirom/control/FingerprintSettingsFragment.kt b/app/src/main/java/org/omnirom/control/FingerprintSettingsFragment.kt
index ab573d0..0245d50 100644
--- a/app/src/main/java/org/omnirom/control/FingerprintSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/FingerprintSettingsFragment.kt
@@ -36,6 +36,8 @@
 import android.util.Log
 import androidx.activity.result.contract.ActivityResultContract
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.preference.Preference
 import androidx.preference.SwitchPreference
 import com.android.systemui.shared.omni.IOmniSystemUiProxy
@@ -49,6 +51,17 @@
     private val FINGERPRINT_CUSTOM_ICON_ENABLE = "custom_fingerprint_icon_enable"
     private val UFPSIMAGE_FILE_NAME = "ufpsImage"
 
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.ic_settings_fingerprint
+        @XmlRes
+        val XML_RES = R.xml.fingerprint_preferences
+
+        fun isEnabled(context: Context) : Boolean {
+            return context.resources.getBoolean(R.bool.config_has_udfps)
+        }
+    }
+
     class PickSinglePhotoContract : ActivityResultContract<Void?, Uri?>() {
         override fun createIntent(context: Context, input: Void?): Intent {
             return Intent(Intent(MediaStore.ACTION_PICK_IMAGES)).apply { type = "image/*" }
@@ -141,7 +154,7 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_settings_fingerprint
+        return ICON_RES
     }
 
     override fun onDestroy() {
@@ -150,7 +163,7 @@
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.fingerprint_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
 
         mEnableCustom = findPreference<SwitchPreference>(FINGERPRINT_CUSTOM_ICON_ENABLE)
         mEnableCustom?.let {
diff --git a/app/src/main/java/org/omnirom/control/GridViewFragment.kt b/app/src/main/java/org/omnirom/control/GridViewFragment.kt
index 81f95e4..df30d71 100644
--- a/app/src/main/java/org/omnirom/control/GridViewFragment.kt
+++ b/app/src/main/java/org/omnirom/control/GridViewFragment.kt
@@ -28,6 +28,7 @@
 import androidx.fragment.app.Fragment
 
 import com.android.internal.util.ArrayUtils;
+import org.omnirom.control.search.SearchFragment
 
 import org.omnirom.omnilibcore.utils.DeviceUtils
 
@@ -80,6 +81,14 @@
         gridItems.clear()
         gridItems.add(
             FragmentGridItem(
+                R.string.search_fragment_title,
+                R.string.search_fragment_summary,
+                R.drawable.ic_search,
+                SearchFragment()
+            )
+        )
+        gridItems.add(
+            FragmentGridItem(
                 R.string.applist_settings_title,
                 R.string.applist_settings_summary,
                 R.drawable.applist_icon,
@@ -331,7 +340,7 @@
                 if (gridItem is FragmentGridItem)
                     requireActivity().supportFragmentManager
                         .beginTransaction()
-                        .replace(R.id.settings, gridItem.gridFragment)
+                        .replace(R.id.settings, gridItem.gridFragment.javaClass, null)
                         .addToBackStack(null)
                         .commit()
                 else if (gridItem is IntentGridItem)
diff --git a/app/src/main/java/org/omnirom/control/LockscreenSettingsFragment.kt b/app/src/main/java/org/omnirom/control/LockscreenSettingsFragment.kt
index 97928d1..006da54 100644
--- a/app/src/main/java/org/omnirom/control/LockscreenSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/LockscreenSettingsFragment.kt
@@ -18,11 +18,18 @@
 package org.omnirom.control
 
 import android.os.Bundle
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.preference.Preference
 
 
 class LockscreenSettingsFragment : AbstractSettingsFragment() {
-
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.ic_lockscreen_tile
+        @XmlRes
+        val XML_RES = R.xml.lockscreen_settings_preferences
+    }
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.lockscreen_item_title)
     }
@@ -32,11 +39,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_lockscreen_tile
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.lockscreen_settings_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
 
         val aodOnCharging: Preference? = findPreference("doze_on_charge")
         if (aodOnCharging != null) {
diff --git a/app/src/main/java/org/omnirom/control/MoreSettingsFragment.kt b/app/src/main/java/org/omnirom/control/MoreSettingsFragment.kt
index 7c6b2e8..e05b28f 100644
--- a/app/src/main/java/org/omnirom/control/MoreSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/MoreSettingsFragment.kt
@@ -18,11 +18,18 @@
 package org.omnirom.control
 
 import android.os.Bundle
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.preference.Preference
 
 
 class MoreSettingsFragment : AbstractSettingsFragment() {
-
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.ic_settings_more
+        @XmlRes
+        val XML_RES = R.xml.more_settings_preferences
+    }
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.more_settings_title)
     }
@@ -32,11 +39,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_settings_more
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.more_settings_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
     }
 
     override fun onPreferenceTreeClick(preference: Preference): Boolean {
diff --git a/app/src/main/java/org/omnirom/control/QSSettingsFragment.kt b/app/src/main/java/org/omnirom/control/QSSettingsFragment.kt
index 8a9e678..79088e1 100644
--- a/app/src/main/java/org/omnirom/control/QSSettingsFragment.kt
+++ b/app/src/main/java/org/omnirom/control/QSSettingsFragment.kt
@@ -19,12 +19,19 @@
 
 import android.os.Bundle
 import android.provider.Settings
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
 import androidx.preference.Preference
 import org.omnirom.omnilib.preference.SeekBarPreference
 
 
 class QSSettingsFragment : AbstractSettingsFragment() {
-
+    companion object {
+        @DrawableRes
+        val ICON_RES = R.drawable.ic_qs_tile
+        @XmlRes
+        val XML_RES = R.xml.qs_settings_preferences
+    }
     override fun getFragmentTitle(): String {
         return resources.getString(R.string.qs_settings_title)
     }
@@ -34,11 +41,11 @@
     }
 
     override fun getFragmentIcon(): Int {
-        return R.drawable.ic_qs_tile
+        return ICON_RES
     }
 
     override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
-        setPreferencesFromResource(R.xml.qs_settings_preferences, rootKey)
+        setPreferencesFromResource(XML_RES, rootKey)
 
         val pm = requireContext().packageManager
         val resources = pm.getResourcesForApplication("com.android.systemui")
diff --git a/app/src/main/java/org/omnirom/control/SettingsActivity.kt b/app/src/main/java/org/omnirom/control/SettingsActivity.kt
index 0758d52..7405e7d 100644
--- a/app/src/main/java/org/omnirom/control/SettingsActivity.kt
+++ b/app/src/main/java/org/omnirom/control/SettingsActivity.kt
@@ -29,6 +29,7 @@
 import androidx.appcompat.widget.Toolbar
 import androidx.core.view.WindowCompat
 import androidx.fragment.app.Fragment
+import org.omnirom.control.search.SearchProvider
 
 
 class SettingsActivity : AppCompatActivity() {
@@ -60,7 +61,7 @@
                 fragment = savedFragment
         }
         supportFragmentManager.beginTransaction()
-            .replace(R.id.settings, fragment)
+            .replace(R.id.settings, fragment.javaClass, null)
             .commit()
 
         val toolbar: Toolbar = findViewById(R.id.toolbar)
diff --git a/app/src/main/java/org/omnirom/control/search/PreferenceXmlParserUtils.java b/app/src/main/java/org/omnirom/control/search/PreferenceXmlParserUtils.java
new file mode 100644
index 0000000..82e48fe
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/search/PreferenceXmlParserUtils.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2017 The Android Open Source 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 org.omnirom.control.search;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.XmlRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.util.Xml;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utility class to parse elements of XML preferences
+ */
+public class PreferenceXmlParserUtils {
+
+    private static final String TAG = "PreferenceXmlParserUtil";
+    @VisibleForTesting
+    static final String PREF_SCREEN_TAG = "PreferenceScreen";
+    private static final List<String> SUPPORTED_PREF_TYPES = Arrays.asList(
+            "Preference");
+    /**
+     * Flag definition to indicate which metadata should be extracted when
+     * {@link #extractMetadata(Context, int, int)} is called. The flags can be combined by using |
+     * (binary or).
+     */
+    @IntDef(flag = true, value = {
+            MetadataFlag.FLAG_INCLUDE_PREF_SCREEN,
+            MetadataFlag.FLAG_NEED_KEY,
+            MetadataFlag.FLAG_NEED_PREF_TYPE,
+            MetadataFlag.FLAG_NEED_PREF_TITLE,
+            MetadataFlag.FLAG_NEED_PREF_SUMMARY,
+            MetadataFlag.FLAG_NEED_PREF_ICON})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface MetadataFlag {
+
+        int FLAG_INCLUDE_PREF_SCREEN = 1;
+        int FLAG_NEED_KEY = 1 << 1;
+        int FLAG_NEED_PREF_TYPE = 1 << 2;
+        int FLAG_NEED_PREF_TITLE = 1 << 3;
+        int FLAG_NEED_PREF_SUMMARY = 1 << 4;
+        int FLAG_NEED_PREF_ICON = 1 << 5;
+    }
+
+    public static final String METADATA_PREF_TYPE = "type";
+    public static final String METADATA_KEY = "key";
+    public static final String METADATA_TITLE = "title";
+    public static final String METADATA_SUMMARY = "summary";
+    public static final String METADATA_ICON = "icon";
+
+    private static final String ENTRIES_SEPARATOR = "|";
+
+    /**
+     * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_KEY} instead.
+     */
+    @Deprecated
+    public static String getDataKey(Context context, AttributeSet attrs) {
+        return getStringData(context, attrs,
+                com.android.internal.R.styleable.Preference,
+                com.android.internal.R.styleable.Preference_key);
+    }
+
+    /**
+     * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_TITLE} instead.
+     */
+    @Deprecated
+    public static String getDataTitle(Context context, AttributeSet attrs) {
+        return getStringData(context, attrs,
+                com.android.internal.R.styleable.Preference,
+                com.android.internal.R.styleable.Preference_title);
+    }
+
+    /**
+     * Call {@link #extractMetadata(Context, int, int)} with {@link #METADATA_SUMMARY} instead.
+     */
+    @Deprecated
+    public static String getDataSummary(Context context, AttributeSet attrs) {
+        return getStringData(context, attrs,
+                com.android.internal.R.styleable.Preference,
+                com.android.internal.R.styleable.Preference_summary);
+    }
+
+    /**
+     * Extracts metadata from preference xml and put them into a {@link Bundle}.
+     *
+     * @param xmlResId xml res id of a preference screen
+     * @param flags    Should be one or more of {@link MetadataFlag}.
+     */
+    @NonNull
+    public static List<Bundle> extractMetadata(Context context, @XmlRes int xmlResId, int flags)
+            throws IOException, XmlPullParserException {
+        final List<Bundle> metadata = new ArrayList<>();
+        if (xmlResId <= 0) {
+            Log.d(TAG, xmlResId + " is invalid.");
+            return metadata;
+        }
+        final XmlResourceParser parser = context.getResources().getXml(xmlResId);
+
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && type != XmlPullParser.START_TAG) {
+            // Parse next until start tag is found
+        }
+        final int outerDepth = parser.getDepth();
+        final boolean hasPrefScreenFlag = hasFlag(flags, MetadataFlag.FLAG_INCLUDE_PREF_SCREEN);
+        do {
+            if (type != XmlPullParser.START_TAG) {
+                continue;
+            }
+            final String nodeName = parser.getName();
+            if (!hasPrefScreenFlag && TextUtils.equals(PREF_SCREEN_TAG, nodeName)) {
+                continue;
+            }
+            if (!SUPPORTED_PREF_TYPES.contains(nodeName) && !nodeName.endsWith("Preference")) {
+                continue;
+            }
+
+            final Bundle preferenceMetadata = new Bundle();
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+            final TypedArray preferenceAttributes = context.obtainStyledAttributes(attrs,
+                    com.android.internal.R.styleable.Preference);
+            TypedArray preferenceScreenAttributes = null;
+            if (hasPrefScreenFlag) {
+                preferenceScreenAttributes = context.obtainStyledAttributes(
+                        attrs, com.android.internal.R.styleable.PreferenceScreen);
+            }
+
+            if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TYPE)) {
+                preferenceMetadata.putString(METADATA_PREF_TYPE, nodeName);
+            }
+            if (hasFlag(flags, MetadataFlag.FLAG_NEED_KEY)) {
+                preferenceMetadata.putString(METADATA_KEY, getKey(preferenceAttributes));
+            }
+            if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_TITLE)) {
+                preferenceMetadata.putString(METADATA_TITLE, getTitle(preferenceAttributes));
+            }
+            if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_SUMMARY)) {
+                preferenceMetadata.putString(METADATA_SUMMARY, getSummary(preferenceAttributes));
+            }
+            if (hasFlag(flags, MetadataFlag.FLAG_NEED_PREF_ICON)) {
+                preferenceMetadata.putInt(METADATA_ICON, getIcon(preferenceAttributes));
+            }
+            Log.d(TAG, "preferenceMetadata = " + preferenceMetadata);
+
+            metadata.add(preferenceMetadata);
+
+            preferenceAttributes.recycle();
+        } while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth));
+        parser.close();
+        return metadata;
+    }
+
+    /**
+     * Call {@link #extractMetadata(Context, int, int)} with a {@link MetadataFlag} instead.
+     */
+    @Deprecated
+    @Nullable
+    private static String getStringData(Context context, AttributeSet set, int[] attrs, int resId) {
+        final TypedArray ta = context.obtainStyledAttributes(set, attrs);
+        String data = ta.getString(resId);
+        ta.recycle();
+        return data;
+    }
+
+    private static boolean hasFlag(int flags, @MetadataFlag int flag) {
+        return (flags & flag) != 0;
+    }
+
+    private static String getDataEntries(Context context, AttributeSet set, int[] attrs,
+            int resId) {
+        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
+        final TypedValue tv = sa.peekValue(resId);
+        sa.recycle();
+        String[] data = null;
+        if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) {
+            if (tv.resourceId != 0) {
+                data = context.getResources().getStringArray(tv.resourceId);
+            }
+        }
+        final int count = (data == null) ? 0 : data.length;
+        if (count == 0) {
+            return null;
+        }
+        final StringBuilder result = new StringBuilder();
+        for (int n = 0; n < count; n++) {
+            result.append(data[n]);
+            result.append(ENTRIES_SEPARATOR);
+        }
+        return result.toString();
+    }
+
+    private static String getKey(TypedArray styledAttributes) {
+        return styledAttributes.getString(com.android.internal.R.styleable.Preference_key);
+    }
+
+    private static String getTitle(TypedArray styledAttributes) {
+        return styledAttributes.getString(com.android.internal.R.styleable.Preference_title);
+    }
+
+    private static String getSummary(TypedArray styledAttributes) {
+        return styledAttributes.getString(com.android.internal.R.styleable.Preference_summary);
+    }
+
+    private static int getIcon(TypedArray styledAttributes) {
+        return styledAttributes.getResourceId(com.android.internal.R.styleable.Icon_icon, 0);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/omnirom/control/search/SearchFragment.kt b/app/src/main/java/org/omnirom/control/search/SearchFragment.kt
new file mode 100644
index 0000000..dd2573e
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/search/SearchFragment.kt
@@ -0,0 +1,109 @@
+/*
+ *  Copyright (C) 2023 The OmniROM Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.omnirom.control.search
+
+import android.content.Context
+import android.os.Bundle
+import android.text.Editable
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import org.omnirom.control.R
+import org.omnirom.control.SettingsActivity
+import org.omnirom.control.widget.TintedDrawableSpan
+
+class SearchFragment : Fragment() {
+    private val listItems = mutableListOf<SearchResult>()
+    private lateinit var adapter: SearchResultAdapter
+
+    fun getFragmentTitle(): String {
+        return resources.getString(R.string.search_fragment_title)
+    }
+
+    fun getFragmentSummary(): String {
+        return resources.getString(R.string.search_fragment_summary)
+    }
+
+    fun getFragmentIcon(): Int {
+        return R.drawable.ic_search
+    }
+
+    override fun onResume() {
+        super.onResume()
+        (activity as SettingsActivity).let {
+            it.showToolbar()
+            it.updateFragmentTitle(
+                getFragmentTitle(),
+                "",
+                getFragmentIcon(),
+                false
+            )
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        return inflater.inflate(R.layout.search_fragment, container, false)
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val resultList: RecyclerView = view.findViewById(R.id.search_result_list)
+        adapter = SearchResultAdapter(listItems, (activity as SettingsActivity))
+        resultList.adapter = adapter
+        resultList.layoutManager = LinearLayoutManager(context)
+
+        val searchText: EditText = view.findViewById(R.id.search_result_string)
+        searchText.addTextChangedListener(object : TextWatcher {
+            override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+            override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+                listItems.clear()
+                if (s.isNotEmpty()) {
+                    SearchProvider.getInstance().getSearchResult(requireContext(), s, listItems)
+                }
+                adapter.notifyDataSetChanged()
+            }
+
+            override fun afterTextChanged(s: Editable) {}
+        })
+    }
+
+    private fun prefixTextWithIcon(
+        context: Context?,
+        iconRes: Int,
+        msg: CharSequence
+    ): CharSequence? {
+        val spanned = SpannableString("  $msg")
+        spanned.setSpan(
+            TintedDrawableSpan(context, iconRes),
+            0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE
+        )
+        return spanned
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/omnirom/control/search/SearchItem.kt b/app/src/main/java/org/omnirom/control/search/SearchItem.kt
new file mode 100644
index 0000000..a2b3200
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/search/SearchItem.kt
@@ -0,0 +1,29 @@
+/*
+ *  Copyright (C) 2023 The OmniROM Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.omnirom.control.search
+
+import android.content.Context
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
+
+data class SearchItem(
+    @XmlRes val xmlResId: Int,
+    val className: String,
+    @DrawableRes val iconResId: Int,
+    val isEnabled: (context: Context) -> Boolean
+)
diff --git a/app/src/main/java/org/omnirom/control/search/SearchProvider.kt b/app/src/main/java/org/omnirom/control/search/SearchProvider.kt
new file mode 100644
index 0000000..1ddf97a
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/search/SearchProvider.kt
@@ -0,0 +1,313 @@
+/*
+ *  Copyright (C) 2023 The OmniROM Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.omnirom.control.search
+
+import android.content.Context
+import android.util.Log
+import androidx.annotation.DrawableRes
+import androidx.annotation.XmlRes
+import org.omnirom.control.AppListFragment
+import org.omnirom.control.BarsSettingsFragment
+import org.omnirom.control.BatteryLightSettingsFragment
+import org.omnirom.control.ButtonSettingsFragment
+import org.omnirom.control.DialerSettingsFragment
+import org.omnirom.control.FingerprintSettingsFragment
+import org.omnirom.control.LockscreenSettingsFragment
+import org.omnirom.control.MoreSettingsFragment
+import org.omnirom.control.QSSettingsFragment
+import org.omnirom.control.search.PreferenceXmlParserUtils.METADATA_KEY
+import org.omnirom.control.search.PreferenceXmlParserUtils.METADATA_SUMMARY
+import org.omnirom.control.search.PreferenceXmlParserUtils.METADATA_TITLE
+import org.omnirom.control.search.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_KEY
+import org.omnirom.control.search.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_PREF_SUMMARY
+import org.omnirom.control.search.PreferenceXmlParserUtils.MetadataFlag.FLAG_NEED_PREF_TITLE
+import org.xmlpull.v1.XmlPullParserException
+import java.io.IOException
+
+
+class SearchProvider private constructor() {
+    private val TAG = "SearchProvider"
+
+    companion object {
+        val NO_SEARCH_INDEX = 0
+
+        private var instance: SearchProvider? = null
+
+        fun getInstance(): SearchProvider {
+            if (instance == null) {
+                instance = SearchProvider()
+            }
+            return instance ?: SearchProvider()
+        }
+    }
+
+    private val searchItemList = mutableListOf<SearchItem>()
+    private val searchResultCache = mutableListOf<SearchResult>()
+
+    private fun isEnabled(context: Context): Boolean {
+        return true
+    }
+
+    init {
+        searchItemList.add(
+            SearchItem(
+                BarsSettingsFragment.XML_RES,
+                BarsSettingsFragment().javaClass.canonicalName,
+                BarsSettingsFragment.ICON_RES,
+                this::isEnabled
+            )
+        )
+        searchItemList.add(
+            SearchItem(
+                MoreSettingsFragment.XML_RES,
+                MoreSettingsFragment().javaClass.canonicalName,
+                MoreSettingsFragment.ICON_RES,
+                this::isEnabled
+            )
+        )
+        searchItemList.add(
+            SearchItem(
+                LockscreenSettingsFragment.XML_RES,
+                LockscreenSettingsFragment().javaClass.canonicalName,
+                LockscreenSettingsFragment.ICON_RES,
+                this::isEnabled
+            )
+        )
+        searchItemList.add(
+            SearchItem(
+                QSSettingsFragment.XML_RES,
+                QSSettingsFragment().javaClass.canonicalName,
+                QSSettingsFragment.ICON_RES,
+                this::isEnabled
+            )
+        )
+
+        searchItemList.add(
+            SearchItem(
+                FingerprintSettingsFragment.XML_RES,
+                FingerprintSettingsFragment().javaClass.canonicalName,
+                FingerprintSettingsFragment.ICON_RES,
+                FingerprintSettingsFragment::isEnabled
+            )
+        )
+
+        searchItemList.add(
+            SearchItem(
+                DialerSettingsFragment.XML_RES,
+                DialerSettingsFragment().javaClass.canonicalName,
+                DialerSettingsFragment.ICON_RES,
+                DialerSettingsFragment::isEnabled
+            )
+        )
+
+        searchItemList.add(
+            SearchItem(
+                ButtonSettingsFragment.XML_RES,
+                ButtonSettingsFragment().javaClass.canonicalName,
+                ButtonSettingsFragment.ICON_RES,
+                this::isEnabled
+            )
+        )
+
+        searchItemList.add(
+            SearchItem(
+                BatteryLightSettingsFragment.XML_RES,
+                BatteryLightSettingsFragment().javaClass.canonicalName,
+                BatteryLightSettingsFragment.ICON_RES,
+                BatteryLightSettingsFragment::isEnabled
+            )
+        )
+
+        searchItemList.add(
+            SearchItem(
+                AppListFragment.XML_RES,
+                AppListFragment().javaClass.canonicalName,
+                AppListFragment.ICON_RES,
+                this::isEnabled
+            )
+        )
+    }
+
+    private fun getKeysFromXml(
+        context: Context, @XmlRes xmlResId: Int
+    ): List<String?>? {
+        val keys: MutableList<String?> = ArrayList()
+        try {
+            val metadata = PreferenceXmlParserUtils.extractMetadata(
+                context,
+                xmlResId, FLAG_NEED_KEY
+            )
+            for (bundle in metadata) {
+                keys.add(bundle.getString(METADATA_KEY))
+            }
+        } catch (e: IOException) {
+            Log.w(TAG, "Error parsing non-indexable from xml $xmlResId")
+        } catch (e: XmlPullParserException) {
+            Log.w(TAG, "Error parsing non-indexable from xml $xmlResId")
+        }
+        return keys
+    }
+
+    private fun buildSearchResultsCache(context: Context) {
+        searchResultCache.clear()
+        searchItemList.forEach {
+            val xml = it.xmlResId
+            val className = it.className
+            if (xml != NO_SEARCH_INDEX) {
+                if (it.isEnabled(context)) {
+                    getAllSearchResultsFromXml(
+                        context,
+                        xml,
+                        className,
+                        it.iconResId,
+                        searchResultCache
+                    )
+                }
+            }
+        }
+    }
+
+    private fun getAllSearchResultsFromXml(
+        context: Context,
+        @XmlRes xmlResId: Int,
+        className: String,
+        @DrawableRes iconResId: Int,
+        items: MutableList<SearchResult>
+    ) {
+        try {
+            val metadata = PreferenceXmlParserUtils.extractMetadata(
+                context,
+                xmlResId, FLAG_NEED_KEY or FLAG_NEED_PREF_TITLE or FLAG_NEED_PREF_SUMMARY
+            )
+            for (bundle in metadata) {
+                val title = bundle.getString(METADATA_TITLE)
+                val summary = bundle.getString(METADATA_SUMMARY)
+                val key = bundle.getString(METADATA_KEY)
+
+                if (title?.isNotEmpty() == true && key?.isNotEmpty() == true) {
+                    items.add(
+                        SearchResult(
+                            title,
+                            summary,
+                            iconResId,
+                            className,
+                            key
+                        )
+                    )
+                }
+            }
+        } catch (e: IOException) {
+            Log.w(TAG, "Error parsing non-indexable from xml $xmlResId")
+        } catch (e: XmlPullParserException) {
+            Log.w(TAG, "Error parsing non-indexable from xml $xmlResId")
+        }
+    }
+
+    private fun getMatchingStringsFromXml(
+        context: Context,
+        @XmlRes xmlResId: Int,
+        className: String,
+        @DrawableRes iconResId: Int,
+        searchString: String,
+        matchingItems: MutableList<SearchResult>
+    ) {
+        try {
+            val metadata = PreferenceXmlParserUtils.extractMetadata(
+                context,
+                xmlResId, FLAG_NEED_KEY or FLAG_NEED_PREF_TITLE or FLAG_NEED_PREF_SUMMARY
+            )
+            for (bundle in metadata) {
+                val title = bundle.getString(METADATA_TITLE)
+                val summary = bundle.getString(METADATA_SUMMARY)
+                val key = bundle.getString(METADATA_KEY)
+
+                if (title?.isNotEmpty() == true && key?.isNotEmpty() == true) {
+                    if (title.contains(
+                            searchString,
+                            ignoreCase = true
+                        ) || summary?.contains(searchString, ignoreCase = true) == true
+                    ) {
+                        matchingItems.add(
+                            SearchResult(
+                                title,
+                                summary,
+                                iconResId,
+                                className,
+                                key
+                            )
+                        )
+                        continue
+                    }
+                }
+            }
+        } catch (e: IOException) {
+            Log.w(TAG, "Error parsing non-indexable from xml $xmlResId")
+        } catch (e: XmlPullParserException) {
+            Log.w(TAG, "Error parsing non-indexable from xml $xmlResId")
+        }
+    }
+
+    fun getSearchResult(
+        context: Context,
+        searchString: CharSequence,
+        result: MutableList<SearchResult>
+    ) {
+        if (searchString.isNotEmpty()) {
+            // TODO maybe persist cache at some point but its so fast right now its not worth the effort
+            if (searchResultCache.isEmpty()) {
+                buildSearchResultsCache(context)
+            }
+            getSearchResultFromCache(searchString, result)
+            /*settingsFragment.forEach {
+                val xml = it.xmlResId
+                val className = it.className
+                if (xml != NO_SEARCH_INDEX) {
+                    if (it.isEnabled(context)) {
+                        getMatchingStringsFromXml(
+                            context,
+                            xml,
+                            className,
+                            it.iconResId,
+                            searchString.toString(),
+                            result
+                        )
+                    }
+                }
+            }*/
+        }
+    }
+
+    private fun getSearchResultFromCache(
+        searchString: CharSequence,
+        result: MutableList<SearchResult>
+    ) {
+        if (searchString.isNotEmpty()) {
+            searchResultCache.forEach {
+                val title = it.title
+                val summary = it.summary
+                if (title?.contains(
+                        searchString,
+                        ignoreCase = true
+                    ) == true || summary?.contains(searchString, ignoreCase = true) == true
+                ) {
+                    result.add(it)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/omnirom/control/search/SearchResult.kt b/app/src/main/java/org/omnirom/control/search/SearchResult.kt
new file mode 100644
index 0000000..88509ff
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/search/SearchResult.kt
@@ -0,0 +1,28 @@
+/*
+ *  Copyright (C) 2023 The OmniROM Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.omnirom.control.search
+
+import androidx.annotation.DrawableRes
+
+data class SearchResult(
+    val title: String?,
+    val summary: String?,
+    @DrawableRes val iconResId: Int,
+    val className: String,
+    val key: String?
+)
diff --git a/app/src/main/java/org/omnirom/control/search/SearchResultAdapter.kt b/app/src/main/java/org/omnirom/control/search/SearchResultAdapter.kt
new file mode 100644
index 0000000..bc8fe92
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/search/SearchResultAdapter.kt
@@ -0,0 +1,77 @@
+/*
+ *  Copyright (C) 2023 The OmniROM Project
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+package org.omnirom.control.search
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.RecyclerView
+import org.omnirom.control.R
+import org.omnirom.control.SettingsActivity
+
+class SearchResultAdapter(val result: List<SearchResult>, val activity: SettingsActivity) :
+    RecyclerView.Adapter<SearchResultAdapter.ViewHolder>() {
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        val item: View = LayoutInflater.from(parent.context)
+            .inflate(R.layout.search_result_item, parent, false)
+        return ViewHolder(item)
+    }
+
+    override fun getItemCount(): Int {
+        return result.size
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val resultItem = result[position]
+        holder.imageView.setImageResource(resultItem.iconResId)
+        holder.titleText.text = resultItem.title ?: ""
+        if (resultItem.summary.isNullOrBlank()) {
+            holder.summaryText.visibility = View.GONE
+        } else {
+            holder.summaryText.visibility = View.VISIBLE
+            holder.summaryText.text = resultItem.summary
+        }
+
+        holder.itemView.setOnClickListener { v ->
+            val bundle = Bundle()
+            bundle.putString(PreferenceXmlParserUtils.METADATA_KEY, resultItem.key)
+            activity.supportFragmentManager
+                .beginTransaction()
+                .replace(
+                    R.id.settings,
+                    Class.forName(resultItem.className) as Class<out Fragment>, bundle
+                )
+                .addToBackStack(null)
+                .commit()
+
+        }
+    }
+
+    class ViewHolder(
+        view: View,
+        val imageView: ImageView = view.findViewById(R.id.search_result_icon),
+        val titleText: TextView = view.findViewById(R.id.search_result_title),
+        val summaryText: TextView = view.findViewById(R.id.search_result_summary)
+    ) : RecyclerView.ViewHolder(view)
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/omnirom/control/widget/TintedDrawableSpan.java b/app/src/main/java/org/omnirom/control/widget/TintedDrawableSpan.java
new file mode 100644
index 0000000..1875938
--- /dev/null
+++ b/app/src/main/java/org/omnirom/control/widget/TintedDrawableSpan.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source 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 org.omnirom.control.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+import android.text.style.DynamicDrawableSpan;
+
+/**
+ * {@link DynamicDrawableSpan} which draws a drawable tinted with the current paint color.
+ */
+public class TintedDrawableSpan extends DynamicDrawableSpan {
+
+    private final Drawable mDrawable;
+    private int mOldTint;
+
+    public TintedDrawableSpan(Context context, int resourceId) {
+        super(ALIGN_BOTTOM);
+        mDrawable = context.getDrawable(resourceId).mutate();
+        mOldTint = 0;
+        mDrawable.setTint(0);
+    }
+
+    @Override
+    public int getSize(Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) {
+        fm = fm == null ? paint.getFontMetricsInt() : fm;
+        int iconSize = fm.bottom - fm.top;
+        mDrawable.setBounds(0, 0, iconSize, iconSize);
+        return super.getSize(paint, text, start, end, fm);
+    }
+
+    @Override
+    public void draw(Canvas canvas, CharSequence text,
+            int start, int end, float x, int top, int y, int bottom, Paint paint) {
+        int color = paint.getColor();
+        if (mOldTint != color) {
+            mOldTint = color;
+            mDrawable.setTint(mOldTint);
+        }
+        super.draw(canvas, text, start, end, x, top, y, bottom, paint);
+    }
+
+    @Override
+    public Drawable getDrawable() {
+        return mDrawable;
+    }
+}
diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml
new file mode 100644
index 0000000..506c8bc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?android:textColorPrimary"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
+</vector>
diff --git a/app/src/main/res/drawable/search_hint_color.xml b/app/src/main/res/drawable/search_hint_color.xml
new file mode 100644
index 0000000..ba37c27
--- /dev/null
+++ b/app/src/main/res/drawable/search_hint_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2016 The Android Open Source 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="@android:color/transparent" android:state_focused="true" />
+    <item android:color="?attr/colorOnSecondaryContainer"/>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/text_field_corners.xml b/app/src/main/res/drawable/text_field_corners.xml
new file mode 100644
index 0000000..3034c1b
--- /dev/null
+++ b/app/src/main/res/drawable/text_field_corners.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="?attr/colorSecondaryContainer" />
+    <corners android:radius="24dp" />
+    <stroke
+        android:width=".8dp"
+        android:color="?attr/colorOutline" />
+</shape>
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_fragment.xml b/app/src/main/res/layout/search_fragment.xml
new file mode 100644
index 0000000..68b41c5
--- /dev/null
+++ b/app/src/main/res/layout/search_fragment.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:layout_marginStart="@dimen/overlay_fragment_side_margin"
+    android:layout_marginEnd="@dimen/overlay_fragment_side_margin"
+    android:orientation="vertical">
+
+    <EditText
+        android:id="@+id/search_result_string"
+        android:layout_width="match_parent"
+        android:layout_height="48dp"
+        android:layout_marginTop="15dp"
+        android:background="@drawable/text_field_corners"
+        android:ellipsize="start"
+        android:gravity="center_vertical"
+        android:hint="@string/search_result_hint"
+        android:paddingStart="16dp"
+        android:singleLine="true"
+        android:textColorHint="@drawable/search_hint_color" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/search_result_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginTop="@dimen/overlay_fragment_side_margin" />
+</LinearLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_result_item.xml b/app/src/main/res/layout/search_result_item.xml
new file mode 100644
index 0000000..7361142
--- /dev/null
+++ b/app/src/main/res/layout/search_result_item.xml
@@ -0,0 +1,45 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="8dp">
+
+    <ImageView
+        android:id="@+id/search_result_icon"
+        android:layout_width="@dimen/grid_item_icon_size"
+        android:layout_height="@dimen/grid_item_icon_size"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="@dimen/grid_icon_margin_start" />
+
+    <LinearLayout
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="start|center_vertical"
+        android:layout_marginStart="@dimen/fragment_icon_margin_start"
+        android:layout_weight="1"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/search_result_title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:gravity="center_vertical"
+            android:maxLines="1"
+            android:textAppearance="@style/Theme.OmniControl.SearchResult.TitleTextStyle"
+            tools:text="aaaaa" />
+
+        <TextView
+            android:id="@+id/search_result_summary"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:gravity="center_vertical"
+            android:maxLines="2"
+            android:textAppearance="@style/Theme.OmniControl.SearchResult.SummaryTextStyle"
+            android:textColor="?android:attr/textColorSecondary"
+            tools:text="bbbbb" />
+
+    </LinearLayout>
+</LinearLayout>
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 903bb4f..4849bbd 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -30,4 +30,6 @@
     <dimen name="toolbar_placeholder_height">105dp</dimen>
     <dimen name="bottom_placeholder_height">60dp</dimen>
     <dimen name="grid_item_margin_start">16dp</dimen>
+    <dimen name="search_result_title_text_size">16sp</dimen>
+    <dimen name="search_result_summary_text_size">12sp</dimen>
 </resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3917599..328c88c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -153,4 +153,8 @@
     <string name="fingerprint_category_custom_icon">Custom icon</string>
     <string name="custom_fp_icon_enable_title">Use custom icon</string>
     <string name="custom_fp_icon_select_title">Select custom icon</string>
+
+    <string name="search_fragment_title">Search</string>
+    <string name="search_fragment_summary">Find settings by text</string>
+    <string name="search_result_hint">Search settings</string>
 </resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index c39d845..6d790d0 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -33,4 +33,12 @@
     <style name="Theme.OmniControl.GridItem.SummaryTextStyle" parent="TextAppearance.Material3.BodyMedium">
         <item name="android:textSize">@dimen/summary_text_size</item>
     </style>
+
+    <style name="Theme.OmniControl.SearchResult.TitleTextStyle" parent="TextAppearance.Material3.BodyMedium">
+        <item name="android:textSize">@dimen/search_result_title_text_size</item>
+    </style>
+
+    <style name="Theme.OmniControl.SearchResult.SummaryTextStyle" parent="TextAppearance.Material3.BodyMedium">
+        <item name="android:textSize">@dimen/search_result_summary_text_size</item>
+    </style>
 </resources>
\ No newline at end of file