cmparts: Make CMParts searchable

 * Implement a SearchIndexablesProvider to allow the Settings app
   to crawl our resources.
 * Add all missing metadata where necessary so resources can be
   indexed.

Change-Id: Ic8f304a7995b269f476eda6306d11b366621f4b0
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 0edd219..b1360d0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -34,6 +34,7 @@
     <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
     <uses-permission android:name="android.permission.BIND_DEVICE_ADMIN" />
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.READ_SEARCH_INDEXABLES" />
 
     <uses-permission android:name="cyanogenmod.permission.BIND_CORE_SERVICE" />
 
@@ -60,6 +61,17 @@
             </intent-filter>
         </receiver>
 
+        <provider android:name=".search.CMPartsSearchIndexablesProvider"
+                  android:authorities="org.cyanogenmod.cmparts"
+                  android:multiprocess="false"
+                  android:grantUriPermissions="true"
+                  android:permission="android.permission.READ_SEARCH_INDEXABLES"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" />
+            </intent-filter>
+        </provider>
+
         <!-- Privacy settings (dashboard) -->
         <activity-alias
             android:name="PrivacySettings"
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 6f018b1..4737694 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -19,11 +19,6 @@
         <attr name="icon" />
     </declare-styleable>
 
-    <!-- For Search -->
-    <declare-styleable name="Preference">
-        <attr name="keywords" format="string" />
-    </declare-styleable>
-
     <!-- For DotsPageIndicator -->
     <declare-styleable name="DotsPageIndicator">
         <attr name="dotDiameter" format="dimension" />
diff --git a/res/xml/anonymous_stats.xml b/res/xml/anonymous_stats.xml
index 30a65d5..b29885c 100644
--- a/res/xml/anonymous_stats.xml
+++ b/res/xml/anonymous_stats.xml
@@ -16,6 +16,7 @@
 
 <PreferenceScreen
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="cmstats"
     android:title="@string/anonymous_statistics_title">
 
     <cyanogenmod.preference.CMSecureSettingSwitchPreference
diff --git a/res/xml/appgroup_list.xml b/res/xml/appgroup_list.xml
index 848ce21..ee497a2 100644
--- a/res/xml/appgroup_list.xml
+++ b/res/xml/appgroup_list.xml
@@ -17,6 +17,6 @@
 <PreferenceScreen
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:key="profile_appgroups_list"
-        xmlns:settings="http://schemas.android.com/apk/res/com.android.settings">
+        android:title="@string/profile_appgroups_title">
 
-</PreferenceScreen>
\ No newline at end of file
+</PreferenceScreen>
diff --git a/res/xml/application_list.xml b/res/xml/application_list.xml
index 43c506f..6df1234 100644
--- a/res/xml/application_list.xml
+++ b/res/xml/application_list.xml
@@ -14,8 +14,7 @@
      limitations under the License.
 -->
 
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
-                  xmlns:settings="http://schemas.android.com/apk/res/com.android.settings">
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
 
     <PreferenceCategory
             android:key="general_section"
@@ -27,4 +26,4 @@
             android:title="@string/profile_applist_title" >
     </PreferenceCategory>
 
-</PreferenceScreen>
\ No newline at end of file
+</PreferenceScreen>
diff --git a/res/xml/battery_light_settings.xml b/res/xml/battery_light_settings.xml
index 34b74d0..454ab1a 100644
--- a/res/xml/battery_light_settings.xml
+++ b/res/xml/battery_light_settings.xml
@@ -15,8 +15,7 @@
 -->
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
-        xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
-        android:key="battery_light"
+        android:key="battery_lights"
         android:title="@string/battery_light_title">
 
     <PreferenceCategory
diff --git a/res/xml/button_settings.xml b/res/xml/button_settings.xml
index c58650e..de9c062 100644
--- a/res/xml/button_settings.xml
+++ b/res/xml/button_settings.xml
@@ -14,7 +14,9 @@
      limitations under the License.
 -->
 
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        android:key="button_settings"
+        android:title="@string/button_pref_title">
 
     <SwitchPreference
         android:key="disable_nav_keys"
diff --git a/res/xml/display_rotation.xml b/res/xml/display_rotation.xml
index b36df56..1ce81e8 100644
--- a/res/xml/display_rotation.xml
+++ b/res/xml/display_rotation.xml
@@ -15,6 +15,7 @@
 -->
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        android:key="rotation"
         android:title="@string/display_rotation_title">
 
     <SwitchPreference
diff --git a/res/xml/livedisplay.xml b/res/xml/livedisplay.xml
index ef1d9bd..443e17a 100644
--- a/res/xml/livedisplay.xml
+++ b/res/xml/livedisplay.xml
@@ -14,7 +14,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+        android:key="livedisplay"
+        android:title="@*cyanogenmod.platform:string/live_display_title">
 
     <PreferenceCategory
         android:key="live_display_options"
diff --git a/res/xml/notification_light_settings.xml b/res/xml/notification_light_settings.xml
index 7035490..b9e0ce1 100644
--- a/res/xml/notification_light_settings.xml
+++ b/res/xml/notification_light_settings.xml
@@ -15,7 +15,7 @@
 -->
 
 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
-        android:key="notification_light"
+        android:key="notification_lights"
         android:title="@string/notification_light_title">
 
     <PreferenceCategory
diff --git a/res/xml/parts_catalog.xml b/res/xml/parts_catalog.xml
index 94d6a7a..8fec12f 100644
--- a/res/xml/parts_catalog.xml
+++ b/res/xml/parts_catalog.xml
@@ -15,15 +15,28 @@
      limitations under the License.
 -->
 
-<parts-catalog xmlns:android="http://schemas.android.com/apk/res/android">
+<!--
+     The parts catalog is used to locate items (usually a PreferenceScreen) inside
+     of CMParts. This can be used by CMPartsPreference to create a simple, two-line
+     entry point from Settings or another application. All entries should specify
+     a fragment, which is a SettingsPreferenceFragment subclass inside CMParts.
+
+     Metadata for the search index provider should be provided for all parts. This
+     can be supplied an XML resource in the "cm:xmlRes" attribute or by implementing
+     the Searchable interface.
+-->
+<parts-catalog xmlns:android="http://schemas.android.com/apk/res/android"
+               xmlns:cm="http://schemas.android.com/apk/res/cyanogenmod.platform">
 
     <part android:key="battery_lights"
           android:title="@string/battery_light_title"
-          android:fragment="org.cyanogenmod.cmparts.notificationlight.BatteryLightSettings" />
+          android:fragment="org.cyanogenmod.cmparts.notificationlight.BatteryLightSettings"
+          cm:xmlRes="@xml/battery_light_settings" />
 
     <part android:key="button_settings"
           android:title="@string/button_pref_title"
-          android:fragment="org.cyanogenmod.cmparts.input.ButtonSettings" />
+          android:fragment="org.cyanogenmod.cmparts.input.ButtonSettings"
+          cm:xmlRes="@xml/button_settings" />
 
     <part android:key="contributors"
           android:title="@string/contributors_cloud_fragment_title"
@@ -32,26 +45,47 @@
     <part android:key="livedisplay"
           android:title="@*cyanogenmod.platform:string/live_display_title"
           android:summary="@string/live_display_summary"
-          android:fragment="org.cyanogenmod.cmparts.livedisplay.LiveDisplay" />
+          android:fragment="org.cyanogenmod.cmparts.livedisplay.LiveDisplay"
+          cm:xmlRes="@xml/livedisplay" />
 
     <part android:key="notification_lights"
           android:title="@string/notification_light_title"
-          android:fragment="org.cyanogenmod.cmparts.notificationlight.NotificationLightSettings" />
+          android:fragment="org.cyanogenmod.cmparts.notificationlight.NotificationLightSettings"
+          cm:xmlRes="@xml/notification_light_settings" />
 
     <part android:key="privacy_settings"
           android:title="@string/privacy_settings_title"
-          android:fragment="org.cyanogenmod.cmparts.PrivacySettings" />
+          android:fragment="org.cyanogenmod.cmparts.PrivacySettings"
+          cm:xmlRes="@xml/privacy_settings" />
 
     <part android:key="profiles_settings"
           android:title="@string/profiles_settings_title"
-          android:fragment="org.cyanogenmod.cmparts.profiles.ProfilesSettings" />
+          android:fragment="org.cyanogenmod.cmparts.profiles.ProfilesSettings"
+          cm:xmlRes="@xml/profiles_settings" />
 
     <part android:key="rotation"
           android:title="@string/display_rotation_title"
-          android:fragment="org.cyanogenmod.cmparts.hardware.DisplayRotation" />
+          android:fragment="org.cyanogenmod.cmparts.hardware.DisplayRotation"
+          cm:xmlRes="@xml/display_rotation" />
 
     <part android:key="status_bar_settings"
           android:title="@string/status_bar_title"
-          android:fragment="org.cyanogenmod.cmparts.statusbar.StatusBarSettings" />
+          android:fragment="org.cyanogenmod.cmparts.statusbar.StatusBarSettings"
+          cm:xmlRes="@xml/status_bar_settings" />
+
+    <part android:key="cmstats"
+          android:title="@string/anonymous_statistics_title"
+          android:fragment="org.cyanogenmod.cmparts.cmstats.AnonymousStats"
+          cm:xmlRes="@xml/anonymous_stats" />
+
+    <part android:key="power_menu"
+          android:title="@string/power_menu_title"
+          android:fragment="org.cyanogenmod.cmparts.input.PowerMenuActions"
+          cm:xmlRes="@xml/power_menu_settings" />
+
+    <part android:key="privacy_guard_manager"
+          android:title="@*cyanogenmod.platform:string/privacy_guard_manager_title"
+          android:fragment="org.cyanogenmod.cmparts.privacyguard.PrivacyGuardManager"
+          cm:xmlRes="@xml/privacy_guard_prefs" />
 
 </parts-catalog>
diff --git a/res/xml/power_menu_settings.xml b/res/xml/power_menu_settings.xml
index 36e1d65..5843c61 100644
--- a/res/xml/power_menu_settings.xml
+++ b/res/xml/power_menu_settings.xml
@@ -16,6 +16,7 @@
 -->
 <PreferenceScreen
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="power_menu"
     android:title="@string/power_menu_title">
 
     <CheckBoxPreference
diff --git a/res/xml/privacy_guard_prefs.xml b/res/xml/privacy_guard_prefs.xml
index f77cdfc..ad91bef 100644
--- a/res/xml/privacy_guard_prefs.xml
+++ b/res/xml/privacy_guard_prefs.xml
@@ -15,7 +15,9 @@
 -->
 
 <PreferenceScreen
-    xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="privacy_guard_manager"
+    android:title="@*cyanogenmod.platform:string/privacy_guard_manager_title">
 
     <SwitchPreference
         android:key="privacy_guard_default"
diff --git a/res/xml/profiles_settings.xml b/res/xml/profiles_settings.xml
index d3c477c..94c6deb 100644
--- a/res/xml/profiles_settings.xml
+++ b/res/xml/profiles_settings.xml
@@ -16,5 +16,5 @@
 
 <PreferenceScreen
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
-    android:key="profiles_list" />
+    android:key="profiles_settings"
+    android:title="@string/profiles_settings_title" />
diff --git a/res/xml/status_bar_settings.xml b/res/xml/status_bar_settings.xml
index 6569250..13f302b 100644
--- a/res/xml/status_bar_settings.xml
+++ b/res/xml/status_bar_settings.xml
@@ -16,6 +16,7 @@
 -->
 <PreferenceScreen
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:key="status_bar_settings"
     android:title="@string/status_bar_title">
 
     <PreferenceScreen
diff --git a/src/org/cyanogenmod/cmparts/contributors/ContributorsCloudFragment.java b/src/org/cyanogenmod/cmparts/contributors/ContributorsCloudFragment.java
index 21864ba..c2cd9ec 100644
--- a/src/org/cyanogenmod/cmparts/contributors/ContributorsCloudFragment.java
+++ b/src/org/cyanogenmod/cmparts/contributors/ContributorsCloudFragment.java
@@ -40,6 +40,7 @@
 import android.text.Html;
 import android.text.TextUtils;
 import android.text.format.DateFormat;
+import android.util.ArraySet;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.Menu;
@@ -50,14 +51,16 @@
 import android.view.WindowManager;
 import android.view.animation.LinearInterpolator;
 import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.SearchView;
 import android.widget.TextView;
-import android.widget.AdapterView.OnItemClickListener;
 
 import org.cyanogenmod.cmparts.R;
+import org.cyanogenmod.cmparts.search.BaseSearchIndexProvider;
+import org.cyanogenmod.cmparts.search.Searchable;
 
 import java.io.File;
 import java.io.FileOutputStream;
@@ -68,9 +71,10 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
+import java.util.Set;
 
 public class ContributorsCloudFragment extends Fragment implements SearchView.OnQueryTextListener,
-        SearchView.OnCloseListener, MenuItem.OnActionExpandListener {
+        SearchView.OnCloseListener, MenuItem.OnActionExpandListener, Searchable {
 
     private static final String TAG = "ContributorsCloud";
 
@@ -767,4 +771,41 @@
             }
         }
     }
+
+    public static final Searchable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+            new BaseSearchIndexProvider() {
+
+                @Override
+                public Set<String> getSearchKeywords(Context context) {
+
+                    // Index the top 100 contributors, for fun :)
+                    File dbPath = context.getDatabasePath(DB_NAME);
+                    SQLiteDatabase db = null;
+                    try {
+                        db = SQLiteDatabase.openDatabase(dbPath.getAbsolutePath(),
+                                null, SQLiteDatabase.OPEN_READONLY);
+                        if (db == null) {
+                            Log.e(TAG, "Cannot open cloud database: " + DB_NAME + ". db == null");
+                            return null;
+                        }
+                    } catch (Exception e) {
+                        Log.e(TAG, e.getMessage(), e);
+                        if (db != null && db.isOpen()) {
+                            db.close();
+                        }
+                        return null;
+                    }
+
+                    Set<String> result = new ArraySet<>();
+                    Cursor c = db.rawQuery(
+                            "select username from metadata order by commits desc limit 100;", null);
+                    while (c.moveToNext()) {
+                        result.add(c.getString(0));
+                    }
+                    c.close();
+                    db.close();
+
+                    return result;
+                }
+            };
 }
diff --git a/src/org/cyanogenmod/cmparts/livedisplay/LiveDisplay.java b/src/org/cyanogenmod/cmparts/livedisplay/LiveDisplay.java
index 5858e59..4b3fec5 100644
--- a/src/org/cyanogenmod/cmparts/livedisplay/LiveDisplay.java
+++ b/src/org/cyanogenmod/cmparts/livedisplay/LiveDisplay.java
@@ -23,20 +23,21 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.UserHandle;
+import android.support.v14.preference.SwitchPreference;
 import android.support.v7.preference.ListPreference;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceCategory;
-import android.support.v14.preference.SwitchPreference;
-import android.provider.SearchIndexableResource;
+import android.util.ArraySet;
 import android.util.Log;
 
 import com.android.internal.util.ArrayUtils;
 
 import org.cyanogenmod.cmparts.R;
 import org.cyanogenmod.cmparts.SettingsPreferenceFragment;
+import org.cyanogenmod.cmparts.search.BaseSearchIndexProvider;
+import org.cyanogenmod.cmparts.search.Searchable;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Set;
 
 import cyanogenmod.hardware.CMHardwareManager;
 import cyanogenmod.hardware.DisplayMode;
@@ -52,7 +53,7 @@
 import static cyanogenmod.hardware.LiveDisplayManager.MODE_OFF;
 import static cyanogenmod.hardware.LiveDisplayManager.MODE_OUTDOOR;
 
-public class LiveDisplay extends SettingsPreferenceFragment implements
+public class LiveDisplay extends SettingsPreferenceFragment implements Searchable,
         Preference.OnPreferenceChangeListener {
 
     private static final String TAG = "LiveDisplay";
@@ -72,6 +73,12 @@
 
     private static final String KEY_LIVE_DISPLAY_COLOR_PROFILE = "live_display_color_profile";
 
+    private static final String COLOR_PROFILE_TITLE =
+            KEY_LIVE_DISPLAY_COLOR_PROFILE + "_%s_title";
+
+    private static final String COLOR_PROFILE_SUMMARY =
+            KEY_LIVE_DISPLAY_COLOR_PROFILE + "_%s_summary";
+
     private final Handler mHandler = new Handler();
     private final SettingsObserver mObserver = new SettingsObserver();
 
@@ -214,8 +221,8 @@
         mObserver.register(false);
     }
 
-    private String getStringForResourceName(String resourceName, String defaultValue) {
-        Resources res = getResources();
+    private static String getStringForResourceName(Resources res,
+                                                   String resourceName, String defaultValue) {
         int resId = res.getIdentifier(resourceName, "string", "org.cyanogenmod.cmparts");
         if (resId <= 0) {
             Log.e(TAG, "No resource found for " + resourceName);
@@ -225,6 +232,18 @@
         }
     }
 
+    private static String getLocalizedProfileName(Resources res, String profileName) {
+        String name = profileName.toLowerCase().replace(" ", "_");
+        String nameRes = String.format(COLOR_PROFILE_TITLE, name);
+        return getStringForResourceName(res, nameRes, profileName);
+    }
+
+    private static String getLocalizedProfileSummary(Resources res, String profileName) {
+        String name = profileName.toLowerCase().replace(" ", "_");
+        String summaryRes = String.format(COLOR_PROFILE_SUMMARY, name);
+        return getStringForResourceName(res, summaryRes, null);
+    }
+
     private boolean updateDisplayModes() {
         final DisplayMode[] modes = mHardware.getDisplayModes();
         if (modes == null || modes.length == 0) {
@@ -239,13 +258,10 @@
         mColorProfileSummaries = new String[modes.length];
         for (int i = 0; i < modes.length; i++) {
             values[i] = String.valueOf(modes[i].id);
-            String name = modes[i].name.toLowerCase().replace(" ", "_");
-            String nameRes = String.format("live_display_color_profile_%s_title", name);
-            entries[i] = getStringForResourceName(nameRes, modes[i].name);
+            entries[i] = getLocalizedProfileName(getResources(), modes[i].name);
 
             // Populate summary
-            String summaryRes = String.format("live_display_color_profile_%s_summary", name);
-            String summary = getStringForResourceName(summaryRes, null);
+            String summary = getLocalizedProfileSummary(getResources(), modes[i].name);
             if (summary != null) {
                 summary = String.format("%s - %s", entries[i], summary);
             }
@@ -360,38 +376,22 @@
 
         @Override
         public void onChange(boolean selfChange, Uri uri) {
-            super.onChange(selfChange,  uri);
+            super.onChange(selfChange, uri);
             updateModeSummary();
             updateTemperatureSummary();
         }
     }
 
-    /*
-     * Disabled until search query is implemented
-     *
-    public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+
+    public static final Searchable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
             new BaseSearchIndexProvider() {
 
         @Override
-        public List<SearchIndexableResource> getXmlResourcesToIndex(Context context,
-                boolean enabled) {
-            ArrayList<SearchIndexableResource> result =
-                    new ArrayList<SearchIndexableResource>();
-
-            SearchIndexableResource sir = new SearchIndexableResource(context);
-            sir.xmlResId = R.xml.livedisplay;
-            result.add(sir);
-
-            return result;
-        }
-
-        @Override
-        public List<String> getNonIndexableKeys(Context context) {
-            final CMHardwareManager hardware = CMHardwareManager.getInstance(context);
+        public Set<String> getNonIndexableKeys(Context context) {
             final LiveDisplayConfig config = LiveDisplayManager.getInstance(context).getConfig();
+            final Set<String> result = new ArraySet<String>();
 
-            ArrayList<String> result = new ArrayList<String>();
-            if (!hardware.isSupported(FEATURE_DISPLAY_MODES)) {
+            if (!config.hasFeature(FEATURE_DISPLAY_MODES)) {
                 result.add(KEY_LIVE_DISPLAY_COLOR_PROFILE);
             }
             if (!config.hasFeature(MODE_OUTDOOR)) {
@@ -411,6 +411,22 @@
             }
             return result;
         }
+
+        @Override
+        public Set<String> getSearchKeywords(Context context) {
+            final LiveDisplayConfig config = LiveDisplayManager.getInstance(context).getConfig();
+            final Set<String> result = new ArraySet<>();
+
+            // Add keywords for supported color profiles
+            if (config.hasFeature(FEATURE_DISPLAY_MODES)) {
+                DisplayMode[] modes = CMHardwareManager.getInstance(context).getDisplayModes();
+                if (modes != null && modes.length > 0) {
+                    for (DisplayMode mode : modes) {
+                        result.add(getLocalizedProfileName(context.getResources(), mode.name));
+                    }
+                }
+            }
+            return result;
+        }
     };
-    */
 }
diff --git a/src/org/cyanogenmod/cmparts/search/BaseSearchIndexProvider.java b/src/org/cyanogenmod/cmparts/search/BaseSearchIndexProvider.java
new file mode 100644
index 0000000..5791c4c
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/search/BaseSearchIndexProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod 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.cyanogenmod.cmparts.search;
+
+import android.content.Context;
+
+import java.util.Set;
+
+/**
+ * Convenience class which can be used to return additional search metadata without
+ * having to implement all methods.
+ */
+public class BaseSearchIndexProvider implements Searchable.SearchIndexProvider {
+
+    @Override
+    public Set<String> getSearchKeywords(Context context) {
+        return null;
+    }
+
+    @Override
+    public Set<String> getNonIndexableKeys(Context context) {
+        return null;
+    }
+}
diff --git a/src/org/cyanogenmod/cmparts/search/CMPartsSearchIndexablesProvider.java b/src/org/cyanogenmod/cmparts/search/CMPartsSearchIndexablesProvider.java
new file mode 100644
index 0000000..5e586b7
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/search/CMPartsSearchIndexablesProvider.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod 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.cyanogenmod.cmparts.search;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.SearchIndexablesProvider;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import org.cyanogenmod.cmparts.PartsActivity;
+import org.cyanogenmod.cmparts.search.Searchable.SearchIndexProvider;
+import org.cyanogenmod.internal.cmparts.PartInfo;
+import org.cyanogenmod.platform.internal.R;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Set;
+
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
+import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS;
+import static android.provider.SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS;
+import static android.provider.SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS;
+import static org.cyanogenmod.internal.cmparts.PartsList.CMPARTS_ACTIVITY;
+import static org.cyanogenmod.internal.cmparts.PartsList.CMPARTS_PACKAGE;
+import static org.cyanogenmod.internal.cmparts.PartsList.getPartInfo;
+import static org.cyanogenmod.internal.cmparts.PartsList.getPartsList;
+
+/**
+ * Provides search metadata to the Settings app
+ */
+public class CMPartsSearchIndexablesProvider extends SearchIndexablesProvider {
+
+    private static final String TAG = CMPartsSearchIndexablesProvider.class.getSimpleName();
+
+    private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
+            "SEARCH_INDEX_DATA_PROVIDER";
+
+    @Override
+    public Cursor queryXmlResources(String[] strings) {
+        MatrixCursor cursor = new MatrixCursor(INDEXABLES_XML_RES_COLUMNS);
+        final Set<String> keys = getPartsList(getContext());
+
+        // return all of the xml resources listed in the resource: attribute
+        // from parts_catalog.xml for indexing
+        for (String key : keys) {
+            PartInfo i = getPartInfo(getContext(), key);
+            if (i == null || i.getXmlRes() <= 0) {
+                continue;
+            }
+
+            Object[] ref = new Object[7];
+            ref[COLUMN_INDEX_XML_RES_RANK] = 2;
+            ref[COLUMN_INDEX_XML_RES_RESID] = i.getXmlRes();
+            ref[COLUMN_INDEX_XML_RES_CLASS_NAME] = null;
+            ref[COLUMN_INDEX_XML_RES_ICON_RESID] = R.drawable.ic_launcher_cyanogenmod;
+            ref[COLUMN_INDEX_XML_RES_INTENT_ACTION] = i.getAction();
+            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE] = CMPARTS_ACTIVITY.getPackageName();
+            ref[COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS] = CMPARTS_ACTIVITY.getClassName();
+            cursor.addRow(ref);
+        }
+        return cursor;
+
+    }
+
+    @Override
+    public Cursor queryRawData(String[] strings) {
+        MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS);
+        final Set<String> keys = getPartsList(getContext());
+
+        // we also submit keywords and metadata for all top-level items
+        // which don't have an associated XML resource
+        for (String key : keys) {
+            PartInfo i = getPartInfo(getContext(), key);
+            if (i == null) {
+                continue;
+            }
+
+            // look for custom keywords
+            SearchIndexProvider sip = getSearchIndexProvider(i.getFragmentClass());
+            if (sip == null) {
+                continue;
+            }
+
+            // don't create a duplicate entry if no custom keywords are provided
+            // and a resource was already indexed
+            Set<String> keywordList = sip.getSearchKeywords(getContext());
+            if ((keywordList == null || keywordList.size() == 0) && i.getXmlRes() > 0) {
+                continue;
+            }
+
+            String keywords = null;
+            if (keywordList != null && keywordList.size() > 0) {
+                keywords = TextUtils.join(" ", keywordList);
+            }
+
+            Object[] ref = new Object[14];
+            ref[COLUMN_INDEX_RAW_RANK] = 2;
+            ref[COLUMN_INDEX_RAW_TITLE] = i.getTitle();
+            ref[COLUMN_INDEX_RAW_SUMMARY_ON] = i.getSummary();
+            ref[COLUMN_INDEX_RAW_KEYWORDS] = keywords;
+            ref[COLUMN_INDEX_RAW_ICON_RESID] = i.getIconRes() > 0 ? i.getIconRes() :
+                    R.drawable.ic_launcher_cyanogenmod;
+            ref[COLUMN_INDEX_RAW_INTENT_ACTION] = i.getAction();
+            ref[COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE] = CMPARTS_ACTIVITY.getPackageName();
+            ref[COLUMN_INDEX_RAW_INTENT_TARGET_CLASS] = CMPARTS_ACTIVITY.getClassName();
+            ref[COLUMN_INDEX_RAW_KEY] = i.getName();
+            ref[COLUMN_INDEX_RAW_USER_ID] = -1;
+            cursor.addRow(ref);
+        }
+        return cursor;
+    }
+
+    @Override
+    public Cursor queryNonIndexableKeys(String[] strings) {
+        MatrixCursor cursor = new MatrixCursor(NON_INDEXABLES_KEYS_COLUMNS);
+
+        final Set<String> keys = getPartsList(getContext());
+        final Set<String> nonIndexables = new ArraySet<>();
+
+        for (String key : keys) {
+            PartInfo i = getPartInfo(getContext(), key);
+            if (i == null) {
+                continue;
+            }
+
+            // look for non-indexable keys
+            SearchIndexProvider sip = getSearchIndexProvider(i.getFragmentClass());
+            if (sip == null) {
+                continue;
+            }
+
+            Set<String> nik = sip.getNonIndexableKeys(getContext());
+            if (nik == null) {
+                continue;
+            }
+
+            nonIndexables.addAll(nik);
+        }
+
+        for (String nik : nonIndexables) {
+            Object[] ref = new Object[1];
+            ref[COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE] = nik;
+            cursor.addRow(ref);
+        }
+        return cursor;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return true;
+    }
+
+    private SearchIndexProvider getSearchIndexProvider(final String className) {
+
+        final Class<?> clazz;
+        try {
+            clazz = Class.forName(className);
+        } catch (ClassNotFoundException e) {
+            Log.d(TAG, "Cannot find class: " + className);
+            return null;
+        }
+
+        if (clazz == null || !Searchable.class.isAssignableFrom(clazz)) {
+            return null;
+        }
+
+        try {
+            final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER);
+            return (SearchIndexProvider) f.get(null);
+        } catch (Exception e) {
+            Log.e(TAG, e.getMessage(), e);
+        }
+        return null;
+    }
+}
diff --git a/src/org/cyanogenmod/cmparts/search/Searchable.java b/src/org/cyanogenmod/cmparts/search/Searchable.java
new file mode 100644
index 0000000..9e69855
--- /dev/null
+++ b/src/org/cyanogenmod/cmparts/search/Searchable.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The CyanogenMod 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.cyanogenmod.cmparts.search;
+
+import android.content.Context;
+
+import java.util.Set;
+
+/**
+ * This interface should be implemented by classes which want to provide additional
+ * dynamic metadata to the indexer. Since our entrypoints are standardized around
+ * the parts catalog, there is no need to enumerate XML resources here. Keywords
+ * and non-indexable keys may be supplied by a class.
+ *
+ * If a class wants to use this functionality, it should contain a static field
+ * named SEARCH_INDEX_DATA_PROVIDER which contains an instance of SearchIndexProvider.
+ * This is similar to the mechanism used by the Settings app.
+ */
+public interface Searchable {
+
+    public interface SearchIndexProvider {
+
+        public Set<String> getSearchKeywords(Context context);
+
+        public Set<String> getNonIndexableKeys(Context context);
+    }
+}