First step in refactoring Index.java

Handles the following refators:
 - Indexing code into: DatabaseIndexingManager
 - Indexing utility methods into: DatabaseIndexingUtil
 - XML Parsiing utility methods into XMLParserUtil

Bug: 33451851
Test: make RunSettingsRoboTests
Change-Id: I4264ad3806d1bd3a66d879c16ad6c8315ecb832b
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index fe25a8f..aa76517 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -253,7 +253,7 @@
             String action = intent.getAction();
             if (action.equals(Intent.ACTION_USER_ADDED)
                     || action.equals(Intent.ACTION_USER_REMOVED)) {
-                Index.getInstance(getApplicationContext()).update();
+                mSearchFeatureProvider.updateIndex(getApplicationContext());
             }
         }
     };
@@ -310,7 +310,7 @@
     @Override
     public void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        Index.getInstance(this).update();
+        mSearchFeatureProvider.updateIndex(getApplicationContext());
     }
 
     @Override
diff --git a/src/com/android/settings/search2/DatabaseIndexingManager.java b/src/com/android/settings/search2/DatabaseIndexingManager.java
new file mode 100644
index 0000000..9bb4538
--- /dev/null
+++ b/src/com/android/settings/search2/DatabaseIndexingManager.java
@@ -0,0 +1,963 @@
+/*
+ * 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 com.android.settings.search2;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.XmlResourceParser;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.SearchIndexableData;
+import android.provider.SearchIndexableResource;
+import android.provider.SearchIndexablesContract;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.settings.search.IndexDatabaseHelper;
+import com.android.settings.search.Indexable;
+import com.android.settings.search.Ranking;
+import com.android.settings.search.SearchIndexableRaw;
+import com.android.settings.search.SearchIndexableResources;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
+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_SCREEN_TITLE;
+import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
+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;
+
+/**
+ * Consumes the SearchIndexableProvider content providers.
+ * Updates the Resource, Raw Data and non-indexable data for Search.
+ */
+public class DatabaseIndexingManager {
+    private static final String LOG_TAG = "DatabaseIndexingManager";
+
+    // Those indices should match the indices of SELECT_COLUMNS !
+    public static final int COLUMN_INDEX_RANK = 0;
+    public static final int COLUMN_INDEX_TITLE = 1;
+    public static final int COLUMN_INDEX_SUMMARY_ON = 2;
+    public static final int COLUMN_INDEX_SUMMARY_OFF = 3;
+    public static final int COLUMN_INDEX_ENTRIES = 4;
+    public static final int COLUMN_INDEX_KEYWORDS = 5;
+    public static final int COLUMN_INDEX_CLASS_NAME = 6;
+    public static final int COLUMN_INDEX_SCREEN_TITLE = 7;
+    public static final int COLUMN_INDEX_ICON = 8;
+    public static final int COLUMN_INDEX_INTENT_ACTION = 9;
+    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 10;
+    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 11;
+    public static final int COLUMN_INDEX_ENABLED = 12;
+    public static final int COLUMN_INDEX_KEY = 13;
+    public static final int COLUMN_INDEX_USER_ID = 14;
+
+    // If you change the order of columns here, you SHOULD change the COLUMN_INDEX_XXX values
+    private static final String[] SELECT_COLUMNS = new String[] {
+            IndexDatabaseHelper.IndexColumns.DATA_RANK,               // 0
+            IndexDatabaseHelper.IndexColumns.DATA_TITLE,              // 1
+            IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON,         // 2
+            IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF,        // 3
+            IndexDatabaseHelper.IndexColumns.DATA_ENTRIES,            // 4
+            IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS,           // 5
+            IndexDatabaseHelper.IndexColumns.CLASS_NAME,              // 6
+            IndexDatabaseHelper.IndexColumns.SCREEN_TITLE,            // 7
+            IndexDatabaseHelper.IndexColumns.ICON,                    // 8
+            IndexDatabaseHelper.IndexColumns.INTENT_ACTION,           // 9
+            IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE,   // 10
+            IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS,     // 11
+            IndexDatabaseHelper.IndexColumns.ENABLED,                 // 12
+            IndexDatabaseHelper.IndexColumns.DATA_KEY_REF             // 13
+    };
+
+    private static final String[] MATCH_COLUMNS_PRIMARY = {
+            IndexDatabaseHelper.IndexColumns.DATA_TITLE,
+            IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED,
+            IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS
+    };
+
+    private static final String[] MATCH_COLUMNS_SECONDARY = {
+            IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON,
+            IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED,
+            IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF,
+            IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
+            IndexDatabaseHelper.IndexColumns.DATA_ENTRIES
+    };
+
+
+    private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
+    private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
+    private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
+
+    private static final List<String> EMPTY_LIST = Collections.<String>emptyList();
+
+    private final String mBaseAuthority;
+
+    /**
+     * A private class to describe the update data for the Index database
+     */
+    private static class UpdateData {
+        public List<SearchIndexableData> dataToUpdate;
+        public List<SearchIndexableData> dataToDelete;
+        public Map<String, List<String>> nonIndexableKeys;
+
+        public boolean forceUpdate;
+        public boolean fullIndex;
+
+        public UpdateData() {
+            dataToUpdate = new ArrayList<SearchIndexableData>();
+            dataToDelete = new ArrayList<SearchIndexableData>();
+            nonIndexableKeys = new HashMap<String, List<String>>();
+        }
+
+        public UpdateData(DatabaseIndexingManager.UpdateData other) {
+            dataToUpdate = new ArrayList<SearchIndexableData>(other.dataToUpdate);
+            dataToDelete = new ArrayList<SearchIndexableData>(other.dataToDelete);
+            nonIndexableKeys = new HashMap<String, List<String>>(other.nonIndexableKeys);
+            forceUpdate = other.forceUpdate;
+            fullIndex = other.fullIndex;
+        }
+
+        public DatabaseIndexingManager.UpdateData copy() {
+            return new DatabaseIndexingManager.UpdateData(this);
+        }
+
+        public void clear() {
+            dataToUpdate.clear();
+            dataToDelete.clear();
+            nonIndexableKeys.clear();
+            forceUpdate = false;
+            fullIndex = false;
+        }
+    }
+
+    private final AtomicBoolean mIsAvailable = new AtomicBoolean(false);
+    private final DatabaseIndexingManager.UpdateData mDataToProcess =
+            new DatabaseIndexingManager.UpdateData();
+    private Context mContext;
+
+    public DatabaseIndexingManager(Context context, String baseAuthority) {
+        mContext = context;
+        mBaseAuthority = baseAuthority;
+    }
+
+    public void setContext(Context context) {
+        mContext = context;
+    }
+
+    public boolean isAvailable() {
+        return mIsAvailable.get();
+    }
+
+    public void update() {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
+                List<ResolveInfo> list =
+                        mContext.getPackageManager().queryIntentContentProviders(intent, 0);
+
+                final int size = list.size();
+                for (int n = 0; n < size; n++) {
+                    final ResolveInfo info = list.get(n);
+                    if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
+                        continue;
+                    }
+                    final String authority = info.providerInfo.authority;
+                    final String packageName = info.providerInfo.packageName;
+
+                    addIndexablesFromRemoteProvider(packageName, authority);
+                    addNonIndexablesKeysFromRemoteProvider(packageName, authority);
+                }
+
+                mDataToProcess.fullIndex = true;
+                updateInternal();
+            }
+        });
+    }
+
+    private boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
+        try {
+            final int baseRank = Ranking.getBaseRankForAuthority(authority);
+
+            final Context context = mBaseAuthority.equals(authority) ?
+                    mContext : mContext.createPackageContext(packageName, 0);
+
+            final Uri uriForResources = buildUriForXmlResources(authority);
+            addIndexablesForXmlResourceUri(context, packageName, uriForResources,
+                    SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank);
+
+            final Uri uriForRawData = buildUriForRawData(authority);
+            addIndexablesForRawDataUri(context, packageName, uriForRawData,
+                    SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank);
+            return true;
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
+                    + Log.getStackTraceString(e));
+            return false;
+        }
+    }
+
+    private void addNonIndexablesKeysFromRemoteProvider(String packageName,
+            String authority) {
+        final List<String> keys =
+                getNonIndexablesKeysFromRemoteProvider(packageName, authority);
+        addNonIndexableKeys(packageName, keys);
+    }
+
+    private List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
+            String authority) {
+        try {
+            final Context packageContext = mContext.createPackageContext(packageName, 0);
+
+            final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
+            return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
+                    SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
+                    + Log.getStackTraceString(e));
+            return EMPTY_LIST;
+        }
+    }
+
+    private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
+            String[] projection) {
+
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, projection, null, null, null);
+
+        if (cursor == null) {
+            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
+            return EMPTY_LIST;
+        }
+
+        List<String> result = new ArrayList<String>();
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
+                    result.add(key);
+                }
+            }
+            return result;
+        } finally {
+            cursor.close();
+        }
+    }
+
+    public void addIndexableData(SearchIndexableData data) {
+        synchronized (mDataToProcess) {
+            mDataToProcess.dataToUpdate.add(data);
+        }
+    }
+
+    public void deleteIndexableData(SearchIndexableData data) {
+        synchronized (mDataToProcess) {
+            mDataToProcess.dataToDelete.add(data);
+        }
+    }
+
+    public void addNonIndexableKeys(String authority, List<String> keys) {
+        synchronized (mDataToProcess) {
+            mDataToProcess.nonIndexableKeys.put(authority, keys);
+        }
+    }
+
+    private void updateFromRemoteProvider(String packageName, String authority) {
+        if (addIndexablesFromRemoteProvider(packageName, authority)) {
+            updateInternal();
+        }
+    }
+
+    /**
+     * Update the Index for a specific class name resources
+     *
+     * @param className the class name (typically a fragment name).
+     * @param rebuild true means that you want to delete the data from the Index first.
+     * @param includeInSearchResults true means that you want the bit "enabled" set so that the
+     *                               data will be seen included into the search results
+     */
+    public void updateFromClassNameResource(String className, final boolean rebuild,
+            boolean includeInSearchResults) {
+        if (className == null) {
+            throw new IllegalArgumentException("class name cannot be null!");
+        }
+        final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className);
+        if (res == null ) {
+            Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className);
+            return;
+        }
+        res.context = mContext;
+        res.enabled = includeInSearchResults;
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                if (rebuild) {
+                    deleteIndexableData(res);
+                }
+                addIndexableData(res);
+                mDataToProcess.forceUpdate = true;
+                updateInternal();
+                res.enabled = false;
+            }
+        });
+    }
+
+    public void updateFromSearchIndexableData(final SearchIndexableData data) {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                addIndexableData(data);
+                mDataToProcess.forceUpdate = true;
+                updateInternal();
+            }
+        });
+    }
+
+    private SQLiteDatabase getReadableDatabase() {
+        return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
+    }
+
+    private SQLiteDatabase getWritableDatabase() {
+        try {
+            return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
+        } catch (SQLiteException e) {
+            Log.e(LOG_TAG, "Cannot open writable database", e);
+            return null;
+        }
+    }
+
+    private static Uri buildUriForXmlResources(String authority) {
+        return Uri.parse("content://" + authority + "/" +
+                SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
+    }
+
+    private static Uri buildUriForRawData(String authority) {
+        return Uri.parse("content://" + authority + "/" +
+                SearchIndexablesContract.INDEXABLES_RAW_PATH);
+    }
+
+    private static Uri buildUriForNonIndexableKeys(String authority) {
+        return Uri.parse("content://" + authority + "/" +
+                SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
+    }
+
+    private void updateInternal() {
+        synchronized (mDataToProcess) {
+            final DatabaseIndexingManager.UpdateIndexTask task =
+                    new DatabaseIndexingManager.UpdateIndexTask();
+            DatabaseIndexingManager.UpdateData copy = mDataToProcess.copy();
+            task.execute(copy);
+            mDataToProcess.clear();
+        }
+    }
+
+    private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
+            Uri uri, String[] projection, int baseRank) {
+
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, projection, null, null, null);
+
+        if (cursor == null) {
+            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
+            return;
+        }
+
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
+                    final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;
+
+                    final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
+
+                    final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
+                    final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
+
+                    final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
+                    final String targetPackage = cursor.getString(
+                            COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
+                    final String targetClass = cursor.getString(
+                            COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
+
+                    SearchIndexableResource sir = new SearchIndexableResource(packageContext);
+                    sir.rank = rank;
+                    sir.xmlResId = xmlResId;
+                    sir.className = className;
+                    sir.packageName = packageName;
+                    sir.iconResId = iconResId;
+                    sir.intentAction = action;
+                    sir.intentTargetPackage = targetPackage;
+                    sir.intentTargetClass = targetClass;
+
+                    addIndexableData(sir);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void addIndexablesForRawDataUri(Context packageContext, String packageName,
+            Uri uri, String[] projection, int baseRank) {
+
+        final ContentResolver resolver = packageContext.getContentResolver();
+        final Cursor cursor = resolver.query(uri, projection, null, null, null);
+
+        if (cursor == null) {
+            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
+            return;
+        }
+
+        try {
+            final int count = cursor.getCount();
+            if (count > 0) {
+                while (cursor.moveToNext()) {
+                    final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK);
+                    final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;
+
+                    final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
+                    final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
+                    final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
+                    final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
+                    final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
+
+                    final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
+
+                    final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
+                    final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
+
+                    final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
+                    final String targetPackage = cursor.getString(
+                            COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
+                    final String targetClass = cursor.getString(
+                            COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
+
+                    final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
+                    final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
+
+                    SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
+                    data.rank = rank;
+                    data.title = title;
+                    data.summaryOn = summaryOn;
+                    data.summaryOff = summaryOff;
+                    data.entries = entries;
+                    data.keywords = keywords;
+                    data.screenTitle = screenTitle;
+                    data.className = className;
+                    data.packageName = packageName;
+                    data.iconResId = iconResId;
+                    data.intentAction = action;
+                    data.intentTargetPackage = targetPackage;
+                    data.intentTargetClass = targetClass;
+                    data.key = key;
+                    data.userId = userId;
+
+                    addIndexableData(data);
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
+            SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) {
+        if (data instanceof SearchIndexableResource) {
+            indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
+        } else if (data instanceof SearchIndexableRaw) {
+            indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
+        }
+    }
+
+    private void indexOneRaw(SQLiteDatabase database, String localeStr,
+            SearchIndexableRaw raw) {
+        // Should be the same locale as the one we are processing
+        if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
+            return;
+        }
+
+        updateOneRowWithFilteredData(database, localeStr,
+                raw.title,
+                raw.summaryOn,
+                raw.summaryOff,
+                raw.entries,
+                raw.className,
+                raw.screenTitle,
+                raw.iconResId,
+                raw.rank,
+                raw.keywords,
+                raw.intentAction,
+                raw.intentTargetPackage,
+                raw.intentTargetClass,
+                raw.enabled,
+                raw.key,
+                raw.userId);
+    }
+
+    private void indexOneResource(SQLiteDatabase database, String localeStr,
+            SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) {
+
+        if (sir == null) {
+            Log.e(LOG_TAG, "Cannot index a null resource!");
+            return;
+        }
+
+        final List<String> nonIndexableKeys = new ArrayList<String>();
+
+        if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) {
+            List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName);
+            if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) {
+                nonIndexableKeys.addAll(resNonIndxableKeys);
+            }
+
+            indexFromResource(sir.context, database, localeStr,
+                    sir.xmlResId, sir.className, sir.iconResId, sir.rank,
+                    sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass,
+                    nonIndexableKeys);
+        } else {
+            if (TextUtils.isEmpty(sir.className)) {
+                Log.w(LOG_TAG, "Cannot index an empty Search Provider name!");
+                return;
+            }
+
+            final Class<?> clazz = DatabaseIndexingUtils.getIndexableClass(sir.className);
+            if (clazz == null) {
+                Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className +
+                        "' should implement the " + Indexable.class.getName() + " interface!");
+                return;
+            }
+
+            // Will be non null only for a Local provider implementing a
+            // SEARCH_INDEX_DATA_PROVIDER field
+            final Indexable.SearchIndexProvider provider =
+                    DatabaseIndexingUtils.getSearchIndexProvider(clazz);
+            if (provider != null) {
+                List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context);
+                if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) {
+                    nonIndexableKeys.addAll(providerNonIndexableKeys);
+                }
+
+                indexFromProvider(mContext, database, localeStr, provider, sir.className,
+                        sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys);
+            }
+        }
+    }
+
+    private void indexFromResource(Context context, SQLiteDatabase database, String localeStr,
+            int xmlResId, String fragmentName, int iconResId, int rank,
+            String intentAction, String intentTargetPackage, String intentTargetClass,
+            List<String> nonIndexableKeys) {
+
+        XmlResourceParser parser = null;
+        try {
+            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
+            }
+
+            String nodeName = parser.getName();
+            if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
+                throw new RuntimeException(
+                        "XML document must start with <PreferenceScreen> tag; found"
+                                + nodeName + " at " + parser.getPositionDescription());
+            }
+
+            final int outerDepth = parser.getDepth();
+            final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+            final String screenTitle = XMLParserUtil.getDataTitle(context, attrs);
+
+            String key = XMLParserUtil.getDataKey(context, attrs);
+
+            String title;
+            String summary;
+            String keywords;
+
+            // Insert rows for the main PreferenceScreen node. Rewrite the data for removing
+            // hyphens.
+            if (!nonIndexableKeys.contains(key)) {
+                title = XMLParserUtil.getDataTitle(context, attrs);
+                summary = XMLParserUtil.getDataSummary(context, attrs);
+                keywords = XMLParserUtil.getDataKeywords(context, attrs);
+
+                updateOneRowWithFilteredData(database, localeStr, title, summary, null, null,
+                        fragmentName, screenTitle, iconResId, rank,
+                        keywords, intentAction, intentTargetPackage, intentTargetClass, true,
+                        key, -1 /* default user id */);
+            }
+
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+
+                nodeName = parser.getName();
+
+                key = XMLParserUtil.getDataKey(context, attrs);
+                if (nonIndexableKeys.contains(key)) {
+                    continue;
+                }
+
+                title = XMLParserUtil.getDataTitle(context, attrs);
+                keywords = XMLParserUtil.getDataKeywords(context, attrs);
+
+                if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
+                    summary = XMLParserUtil.getDataSummary(context, attrs);
+
+                    String entries = null;
+
+                    if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
+                        entries = XMLParserUtil.getDataEntries(context, attrs);
+                    }
+
+                    // Insert rows for the child nodes of PreferenceScreen
+                    updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries,
+                            fragmentName, screenTitle, iconResId, rank,
+                            keywords, intentAction, intentTargetPackage, intentTargetClass,
+                            true, key, -1 /* default user id */);
+                } else {
+                    String summaryOn = XMLParserUtil.getDataSummaryOn(context, attrs);
+                    String summaryOff = XMLParserUtil.getDataSummaryOff(context, attrs);
+
+                    if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) {
+                        summaryOn = XMLParserUtil.getDataSummary(context, attrs);
+                    }
+
+                    updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff,
+                            null, fragmentName, screenTitle, iconResId, rank,
+                            keywords, intentAction, intentTargetPackage, intentTargetClass,
+                            true, key, -1 /* default user id */);
+                }
+            }
+
+        } catch (XmlPullParserException e) {
+            throw new RuntimeException("Error parsing PreferenceScreen", e);
+        } catch (IOException e) {
+            throw new RuntimeException("Error parsing PreferenceScreen", e);
+        } finally {
+            if (parser != null) parser.close();
+        }
+    }
+
+    private void indexFromProvider(Context context, SQLiteDatabase database, String localeStr,
+            Indexable.SearchIndexProvider provider, String className, int iconResId, int rank,
+            boolean enabled, List<String> nonIndexableKeys) {
+
+        if (provider == null) {
+            Log.w(LOG_TAG, "Cannot find provider: " + className);
+            return;
+        }
+
+        final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(context, enabled);
+
+        if (rawList != null) {
+            final int rawSize = rawList.size();
+            for (int i = 0; i < rawSize; i++) {
+                SearchIndexableRaw raw = rawList.get(i);
+
+                // Should be the same locale as the one we are processing
+                if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
+                    continue;
+                }
+
+                if (nonIndexableKeys.contains(raw.key)) {
+                    continue;
+                }
+
+                updateOneRowWithFilteredData(database, localeStr,
+                        raw.title,
+                        raw.summaryOn,
+                        raw.summaryOff,
+                        raw.entries,
+                        className,
+                        raw.screenTitle,
+                        iconResId,
+                        rank,
+                        raw.keywords,
+                        raw.intentAction,
+                        raw.intentTargetPackage,
+                        raw.intentTargetClass,
+                        raw.enabled,
+                        raw.key,
+                        raw.userId);
+            }
+        }
+
+        final List<SearchIndexableResource> resList =
+                provider.getXmlResourcesToIndex(context, enabled);
+        if (resList != null) {
+            final int resSize = resList.size();
+            for (int i = 0; i < resSize; i++) {
+                SearchIndexableResource item = resList.get(i);
+
+                // Should be the same locale as the one we are processing
+                if (!item.locale.toString().equalsIgnoreCase(localeStr)) {
+                    continue;
+                }
+
+                final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId;
+                final int itemRank = (item.rank == 0) ? rank : item.rank;
+                String itemClassName = (TextUtils.isEmpty(item.className))
+                        ? className : item.className;
+
+                indexFromResource(context, database, localeStr,
+                        item.xmlResId, itemClassName, itemIconResId, itemRank,
+                        item.intentAction, item.intentTargetPackage,
+                        item.intentTargetClass, nonIndexableKeys);
+            }
+        }
+    }
+
+    private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale,
+            String title, String summaryOn, String summaryOff, String entries,
+            String className,
+            String screenTitle, int iconResId, int rank, String keywords,
+            String intentAction, String intentTargetPackage, String intentTargetClass,
+            boolean enabled, String key, int userId) {
+
+        final String updatedTitle = XMLParserUtil.normalizeHyphen(title);
+        final String updatedSummaryOn = XMLParserUtil.normalizeHyphen(summaryOn);
+        final String updatedSummaryOff = XMLParserUtil.normalizeHyphen(summaryOff);
+
+        final String normalizedTitle = XMLParserUtil.normalizeString(updatedTitle);
+        final String normalizedSummaryOn = XMLParserUtil.normalizeString(updatedSummaryOn);
+        final String normalizedSummaryOff = XMLParserUtil.normalizeString(updatedSummaryOff);
+
+        final String spaceDelimitedKeywords = XMLParserUtil.normalizeKeywords(keywords);
+
+        updateOneRow(database, locale,
+                updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn,
+                updatedSummaryOff, normalizedSummaryOff, entries, className, screenTitle, iconResId,
+                rank, spaceDelimitedKeywords, intentAction, intentTargetPackage, intentTargetClass,
+                enabled, key, userId);
+    }
+
+    private void updateOneRow(SQLiteDatabase database, String locale, String updatedTitle,
+            String normalizedTitle, String updatedSummaryOn, String normalizedSummaryOn,
+            String updatedSummaryOff, String normalizedSummaryOff, String entries, String className,
+            String screenTitle, int iconResId, int rank, String spaceDelimitedKeywords,
+            String intentAction, String intentTargetPackage, String intentTargetClass,
+            boolean enabled, String key, int userId) {
+
+        if (TextUtils.isEmpty(updatedTitle)) {
+            return;
+        }
+
+        // The DocID should contains more than the title string itself (you may have two settings
+        // with the same title). So we need to use a combination of the title and the screenTitle.
+        StringBuilder sb = new StringBuilder(updatedTitle);
+        sb.append(screenTitle);
+        int docId = sb.toString().hashCode();
+
+        ContentValues values = new ContentValues();
+        values.put(IndexDatabaseHelper.IndexColumns.DOCID, docId);
+        values.put(IndexDatabaseHelper.IndexColumns.LOCALE, locale);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, rank);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, updatedTitle);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED,
+                normalizedSummaryOn);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
+                normalizedSummaryOff);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_ENTRIES, entries);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS, spaceDelimitedKeywords);
+        values.put(IndexDatabaseHelper.IndexColumns.CLASS_NAME, className);
+        values.put(IndexDatabaseHelper.IndexColumns.SCREEN_TITLE, screenTitle);
+        values.put(IndexDatabaseHelper.IndexColumns.INTENT_ACTION, intentAction);
+        values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage);
+        values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS, intentTargetClass);
+        values.put(IndexDatabaseHelper.IndexColumns.ICON, iconResId);
+        values.put(IndexDatabaseHelper.IndexColumns.ENABLED, enabled);
+        values.put(IndexDatabaseHelper.IndexColumns.DATA_KEY_REF, key);
+        values.put(IndexDatabaseHelper.IndexColumns.USER_ID, userId);
+
+        database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values);
+    }
+
+    /**
+     * A private class for updating the Index database
+     */
+    private class UpdateIndexTask extends AsyncTask<DatabaseIndexingManager.UpdateData, Integer,
+            Void> {
+
+        @Override
+        protected void onPreExecute() {
+            super.onPreExecute();
+            mIsAvailable.set(false);
+        }
+
+        @Override
+        protected void onPostExecute(Void aVoid) {
+            super.onPostExecute(aVoid);
+            mIsAvailable.set(true);
+        }
+
+        @Override
+        protected Void doInBackground(DatabaseIndexingManager.UpdateData... params) {
+            try {
+                final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate;
+                final List<SearchIndexableData> dataToDelete = params[0].dataToDelete;
+                final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys;
+
+                final boolean forceUpdate = params[0].forceUpdate;
+                final boolean fullIndex = params[0].fullIndex;
+
+                final SQLiteDatabase database = getWritableDatabase();
+                if (database == null) {
+                    Log.e(LOG_TAG, "Cannot update Index as I cannot get a writable database");
+                    return null;
+                }
+                final String localeStr = Locale.getDefault().toString();
+
+                try {
+                    database.beginTransaction();
+                    if (dataToDelete.size() > 0) {
+                        processDataToDelete(database, localeStr, dataToDelete);
+                    }
+                    if (dataToUpdate.size() > 0) {
+                        processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys,
+                                forceUpdate);
+                    }
+                    database.setTransactionSuccessful();
+                } finally {
+                    database.endTransaction();
+                }
+                if (fullIndex) {
+                    IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
+                }
+            } catch (SQLiteFullException e) {
+                Log.e(LOG_TAG, "Unable to index search, out of space", e);
+            }
+
+            return null;
+        }
+
+        private boolean processDataToUpdate(SQLiteDatabase database, String localeStr,
+                List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys,
+                boolean forceUpdate) {
+
+            if (!forceUpdate && IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)) {
+                Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed");
+                return true;
+            }
+
+            boolean result = false;
+            final long current = System.currentTimeMillis();
+
+            final int count = dataToUpdate.size();
+            for (int n = 0; n < count; n++) {
+                final SearchIndexableData data = dataToUpdate.get(n);
+                try {
+                    indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys);
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Cannot index: " + (data != null ? data.className : data)
+                            + " for locale: " + localeStr, e);
+                }
+            }
+
+            final long now = System.currentTimeMillis();
+            Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
+                    (now - current) + " millis");
+            return result;
+        }
+
+        private boolean processDataToDelete(SQLiteDatabase database, String localeStr,
+                List<SearchIndexableData> dataToDelete) {
+
+            boolean result = false;
+            final long current = System.currentTimeMillis();
+
+            final int count = dataToDelete.size();
+            for (int n = 0; n < count; n++) {
+                final SearchIndexableData data = dataToDelete.get(n);
+                if (data == null) {
+                    continue;
+                }
+                if (!TextUtils.isEmpty(data.className)) {
+                    delete(database, IndexDatabaseHelper.IndexColumns.CLASS_NAME, data.className);
+                } else  {
+                    if (data instanceof SearchIndexableRaw) {
+                        final SearchIndexableRaw raw = (SearchIndexableRaw) data;
+                        if (!TextUtils.isEmpty(raw.title)) {
+                            delete(database, IndexDatabaseHelper.IndexColumns.DATA_TITLE,
+                                    raw.title);
+                        }
+                    }
+                }
+            }
+
+            final long now = System.currentTimeMillis();
+            Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " +
+                    (now - current) + " millis");
+            return result;
+        }
+
+        private int delete(SQLiteDatabase database, String columName, String value) {
+            final String whereClause = columName + "=?";
+            final String[] whereArgs = new String[] { value };
+
+            return database.delete(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, whereClause,
+                    whereArgs);
+        }
+    }
+}
diff --git a/src/com/android/settings/search2/DatabaseIndexingUtils.java b/src/com/android/settings/search2/DatabaseIndexingUtils.java
new file mode 100644
index 0000000..a8f64df
--- /dev/null
+++ b/src/com/android/settings/search2/DatabaseIndexingUtils.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.android.settings.search2;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.settings.search.Indexable;
+
+import java.lang.reflect.Field;
+
+/**
+ * Utility class for {@like DatabaseIndexingManager} to handle the mapping between Payloads
+ * and Preference controllers, and managing indexable classes.
+ */
+public class DatabaseIndexingUtils {
+
+    private static final String LOG_TAG = "IndexingUtil";
+
+    private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
+            "SEARCH_INDEX_DATA_PROVIDER";
+
+    public static Class<?> getIndexableClass(String className) {
+        final Class<?> clazz;
+        try {
+            clazz = Class.forName(className);
+        } catch (ClassNotFoundException e) {
+            Log.d(LOG_TAG, "Cannot find class: " + className);
+            return null;
+        }
+        return isIndexableClass(clazz) ? clazz : null;
+    }
+
+    public static boolean isIndexableClass(final Class<?> clazz) {
+        return (clazz != null) && Indexable.class.isAssignableFrom(clazz);
+    }
+
+    public static Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) {
+        try {
+            final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER);
+            return (Indexable.SearchIndexProvider) f.get(null);
+        } catch (NoSuchFieldException e) {
+            Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
+        } catch (SecurityException se) {
+            Log.d(LOG_TAG,
+                    "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
+        } catch (IllegalAccessException e) {
+            Log.d(LOG_TAG,
+                    "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
+        } catch (IllegalArgumentException e) {
+            Log.d(LOG_TAG,
+                    "Illegal argument when accessing field '" +
+                            FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
+        }
+        return null;
+    }
+
+    /**
+     * Only allow a "well known" SearchIndexablesProvider. The provider should:
+     *
+     * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES}
+     * - be from a privileged package
+     */
+    public static boolean isWellKnownProvider(ResolveInfo info, Context context) {
+        final String authority = info.providerInfo.authority;
+        final String packageName = info.providerInfo.applicationInfo.packageName;
+
+        if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) {
+            return false;
+        }
+
+        final String readPermission = info.providerInfo.readPermission;
+        final String writePermission = info.providerInfo.writePermission;
+
+        if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) {
+            return false;
+        }
+
+        if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) ||
+                !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) {
+            return false;
+        }
+
+        return isPrivilegedPackage(packageName, context);
+    }
+
+    public static boolean isPrivilegedPackage(String packageName, Context context) {
+        final PackageManager pm = context.getPackageManager();
+        try {
+            PackageInfo packInfo = pm.getPackageInfo(packageName, 0);
+            return ((packInfo.applicationInfo.privateFlags
+                    & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java
index da29c85..ad26eae 100644
--- a/src/com/android/settings/search2/SearchFeatureProvider.java
+++ b/src/com/android/settings/search2/SearchFeatureProvider.java
@@ -46,4 +46,14 @@
      * Returns a new loader to search installed apps.
      */
     InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query);
+
+    /**
+     * Returns the manager for indexing Settings data.
+     */
+    DatabaseIndexingManager getIndexingManager(Context context);
+
+    /**
+     * Updates the Settings indexes
+     */
+    void updateIndex(Context context);
 }
diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
index 7203049..e2d25ad 100644
--- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java
+++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
@@ -23,6 +23,8 @@
 import android.view.MenuItem;
 
 import com.android.settings.R;
+import com.android.settings.search.Index;
+
 import com.android.settings.applications.PackageManagerWrapperImpl;
 
 /**
@@ -31,6 +33,7 @@
 public class SearchFeatureProviderImpl implements SearchFeatureProvider {
     protected Context mContext;
 
+    private DatabaseIndexingManager mDatabaseIndexingManager;
 
     public SearchFeatureProviderImpl(Context context) {
         mContext = context;
@@ -71,4 +74,22 @@
         return new InstalledAppResultLoader(
                 context, new PackageManagerWrapperImpl(context.getPackageManager()), query);
     }
+
+    @Override
+    public DatabaseIndexingManager getIndexingManager(Context context) {
+        if (mDatabaseIndexingManager == null) {
+            mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext(),
+                    context.getPackageName());
+        }
+        return mDatabaseIndexingManager;
+    }
+
+    @Override
+    public void updateIndex(Context context) {
+        if (isEnabled()) {
+            getIndexingManager(context).update();
+        } else {
+            Index.getInstance(context).update();
+        }
+    }
 }
diff --git a/src/com/android/settings/search2/XMLParserUtil.java b/src/com/android/settings/search2/XMLParserUtil.java
new file mode 100644
index 0000000..dcb3cbb
--- /dev/null
+++ b/src/com/android/settings/search2/XMLParserUtil.java
@@ -0,0 +1,137 @@
+/*
+ * 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 com.android.settings.search2;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+
+import com.android.settings.R;
+
+import java.text.Normalizer;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class to parse elements of XML preferences
+ */
+public class XMLParserUtil {
+
+    private static final String NON_BREAKING_HYPHEN = "\u2011";
+    private static final String EMPTY = "";
+    private static final String LIST_DELIMITERS = "[,]\\s*";
+    private static final String HYPHEN = "-";
+    private static final String SPACE = " ";
+
+    private static final String ENTRIES_SEPARATOR = "|";
+
+    private static final Pattern REMOVE_DIACRITICALS_PATTERN
+            = Pattern.compile("\\p{InCombiningDiacriticalMarks}+");
+
+    public static String getDataKey(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                com.android.internal.R.styleable.Preference,
+                com.android.internal.R.styleable.Preference_key);
+    }
+
+    public static String getDataTitle(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                com.android.internal.R.styleable.Preference,
+                com.android.internal.R.styleable.Preference_title);
+    }
+
+    public static String getDataSummary(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                com.android.internal.R.styleable.Preference,
+                com.android.internal.R.styleable.Preference_summary);
+    }
+
+    public static String getDataSummaryOn(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                com.android.internal.R.styleable.CheckBoxPreference,
+                com.android.internal.R.styleable.CheckBoxPreference_summaryOn);
+    }
+
+    public static String getDataSummaryOff(Context context, AttributeSet attrs) {
+        return getData(context, attrs,
+                com.android.internal.R.styleable.CheckBoxPreference,
+                com.android.internal.R.styleable.CheckBoxPreference_summaryOff);
+    }
+
+    public static String getDataEntries(Context context, AttributeSet attrs) {
+        return getDataEntries(context, attrs,
+                com.android.internal.R.styleable.ListPreference,
+                com.android.internal.R.styleable.ListPreference_entries);
+    }
+
+    public static String getDataKeywords(Context context, AttributeSet attrs) {
+        return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords);
+    }
+
+    public static String getData(Context context, AttributeSet set, int[] attrs, int resId) {
+        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
+        final TypedValue tv = sa.peekValue(resId);
+
+        CharSequence data = null;
+        if (tv != null && tv.type == TypedValue.TYPE_STRING) {
+            if (tv.resourceId != 0) {
+                data = context.getText(tv.resourceId);
+            } else {
+                data = tv.string;
+            }
+        }
+        return (data != null) ? data.toString() : null;
+    }
+
+    public static String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) {
+        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
+        final TypedValue tv = sa.peekValue(resId);
+
+        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();
+    }
+
+    public static String normalizeHyphen(String input) {
+        return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY;
+    }
+
+    public static String normalizeString(String input) {
+        final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY;
+        final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD);
+
+        return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase();
+    }
+
+    public static String normalizeKeywords(String input) {
+        return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY;
+    }
+}