Home | History | Annotate | Download | only in search2
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  *
     16  */
     17 
     18 package com.android.settings.search2;
     19 
     20 import android.content.ContentResolver;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.pm.PackageManager;
     25 import android.content.pm.ResolveInfo;
     26 import android.content.res.XmlResourceParser;
     27 import android.database.Cursor;
     28 import android.database.sqlite.SQLiteDatabase;
     29 import android.database.sqlite.SQLiteException;
     30 import android.net.Uri;
     31 import android.os.AsyncTask;
     32 import android.os.Build;
     33 import android.provider.SearchIndexableData;
     34 import android.provider.SearchIndexableResource;
     35 import android.provider.SearchIndexablesContract;
     36 import android.support.annotation.DrawableRes;
     37 import android.support.annotation.VisibleForTesting;
     38 import android.text.TextUtils;
     39 import android.util.AttributeSet;
     40 import android.util.Log;
     41 import android.util.Xml;
     42 
     43 import com.android.settings.core.PreferenceController;
     44 import com.android.settings.search.IndexDatabaseHelper;
     45 import com.android.settings.search.Indexable;
     46 import com.android.settings.search.IndexingCallback;
     47 import com.android.settings.search.SearchIndexableRaw;
     48 import com.android.settings.search.SearchIndexableResources;
     49 
     50 import org.xmlpull.v1.XmlPullParser;
     51 import org.xmlpull.v1.XmlPullParserException;
     52 
     53 import java.io.IOException;
     54 import java.util.ArrayList;
     55 import java.util.Collections;
     56 import java.util.HashMap;
     57 import java.util.HashSet;
     58 import java.util.List;
     59 import java.util.Locale;
     60 import java.util.Map;
     61 import java.util.Objects;
     62 import java.util.Set;
     63 import java.util.concurrent.atomic.AtomicBoolean;
     64 
     65 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
     66 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
     67 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
     68 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
     69 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
     70 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
     71 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
     72 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
     73 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
     74 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK;
     75 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
     76 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
     77 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
     78 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
     79 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
     80 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
     81 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
     82 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
     83 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
     84 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
     85 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK;
     86 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
     87 
     88 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
     89 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
     90 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
     91 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEY_REF;
     92 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_RANK;
     93 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF;
     94 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED;
     95 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON;
     96 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED;
     97 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
     98 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
     99 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
    100 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
    101 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
    102 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
    103 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS;
    104 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE;
    105 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.LOCALE;
    106 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD;
    107 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.PAYLOAD_TYPE;
    108 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.SCREEN_TITLE;
    109 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ID;
    110 import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX;
    111 
    112 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_ID;
    113 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
    114 import static com.android.settings.search2.DatabaseResultLoader.COLUMN_INDEX_KEY;
    115 import static com.android.settings.search2.DatabaseResultLoader.SELECT_COLUMNS;
    116 
    117 /**
    118  * Consumes the SearchIndexableProvider content providers.
    119  * Updates the Resource, Raw Data and non-indexable data for Search.
    120  *
    121  * TODO this class needs to be refactored by moving most of its methods into controllers
    122  */
    123 public class DatabaseIndexingManager {
    124     private static final String LOG_TAG = "DatabaseIndexingManager";
    125 
    126     public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
    127             "SEARCH_INDEX_DATA_PROVIDER";
    128 
    129     private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
    130     private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
    131     private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
    132 
    133     private static final List<String> EMPTY_LIST = Collections.emptyList();
    134 
    135     private final String mBaseAuthority;
    136 
    137     @VisibleForTesting
    138     final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
    139 
    140     @VisibleForTesting
    141     final UpdateData mDataToProcess = new UpdateData();
    142     private Context mContext;
    143 
    144     public DatabaseIndexingManager(Context context, String baseAuthority) {
    145         mContext = context;
    146         mBaseAuthority = baseAuthority;
    147     }
    148 
    149     public void setContext(Context context) {
    150         mContext = context;
    151     }
    152 
    153     public boolean isIndexingComplete() {
    154         return mIsIndexingComplete.get();
    155     }
    156 
    157     public void indexDatabase(IndexingCallback callback) {
    158         IndexingTask task = new IndexingTask(callback);
    159         task.execute();
    160     }
    161 
    162     /**
    163      * Accumulate all data and non-indexable keys from each of the content-providers.
    164      * Only the first indexing for the default language gets static search results - subsequent
    165      * calls will only gather non-indexable keys.
    166      */
    167     @VisibleForTesting
    168     void performIndexing() {
    169         final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
    170         final List<ResolveInfo> list =
    171                 mContext.getPackageManager().queryIntentContentProviders(intent, 0);
    172 
    173         String localeStr = Locale.getDefault().toString();
    174         String fingerprint = Build.FINGERPRINT;
    175         final boolean isFullIndex = isFullIndex(localeStr, fingerprint);
    176 
    177         if (isFullIndex) {
    178             rebuildDatabase();
    179         }
    180 
    181         for (final ResolveInfo info : list) {
    182             if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
    183                 continue;
    184             }
    185             final String authority = info.providerInfo.authority;
    186             final String packageName = info.providerInfo.packageName;
    187 
    188             if (isFullIndex) {
    189                 addIndexablesFromRemoteProvider(packageName, authority);
    190             }
    191             addNonIndexablesKeysFromRemoteProvider(packageName, authority);
    192         }
    193 
    194         updateDatabase(isFullIndex, localeStr);
    195 
    196         IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
    197         IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
    198     }
    199 
    200     /**
    201      * Perform a full index on an OTA or when the locale has changed
    202      *
    203      * @param locale is the default for the device
    204      * @param fingerprint id for the current build.
    205      * @return true when the locale or build has changed since last index.
    206      */
    207     @VisibleForTesting
    208     boolean isFullIndex(String locale, String fingerprint) {
    209         final boolean isLocaleIndexed = IndexDatabaseHelper.getInstance(mContext)
    210                 .isLocaleAlreadyIndexed(mContext, locale);
    211         final boolean isBuildIndexed = IndexDatabaseHelper.getInstance(mContext)
    212                 .isBuildIndexed(mContext, fingerprint);
    213         return !isLocaleIndexed || !isBuildIndexed;
    214     }
    215 
    216     /**
    217      * Reconstruct the database in the following cases:
    218      * - Language has changed
    219      * - Build has changed
    220      */
    221     private void rebuildDatabase() {
    222         // Drop the database when the locale or build has changed. This eliminates rows which are
    223         // dynamically inserted in the old language, or deprecated settings.
    224         final SQLiteDatabase db = getWritableDatabase();
    225         IndexDatabaseHelper.getInstance(mContext).reconstruct(db);
    226     }
    227 
    228     /**
    229      * Adds new data to the database and verifies the correctness of the ENABLED column.
    230      * First, the data to be updated and all non-indexable keys are copied locally.
    231      * Then all new data to be added is inserted.
    232      * Then search results are verified to have the correct value of enabled.
    233      * Finally, we record that the locale has been indexed.
    234      *
    235      * @param needsReindexing true the database needs to be rebuilt.
    236      * @param localeStr the default locale for the device.
    237      */
    238     @VisibleForTesting
    239     void updateDatabase(boolean needsReindexing, String localeStr) {
    240         final UpdateData copy;
    241 
    242         synchronized (mDataToProcess) {
    243             copy = mDataToProcess.copy();
    244             mDataToProcess.clear();
    245         }
    246 
    247         final List<SearchIndexableData> dataToUpdate = copy.dataToUpdate;
    248         final Map<String, Set<String>> nonIndexableKeys = copy.nonIndexableKeys;
    249 
    250         final SQLiteDatabase database = getWritableDatabase();
    251         if (database == null) {
    252             Log.w(LOG_TAG, "Cannot indexDatabase Index as I cannot get a writable database");
    253             return;
    254         }
    255 
    256         try {
    257             database.beginTransaction();
    258 
    259             // Add new data from Providers at initial index time, or inserted later.
    260             if (dataToUpdate.size() > 0) {
    261                 addDataToDatabase(database, localeStr, dataToUpdate, nonIndexableKeys);
    262             }
    263 
    264             // Only check for non-indexable key updates after initial index.
    265             // Enabled state with non-indexable keys is checked when items are first inserted.
    266             if (!needsReindexing) {
    267                 updateDataInDatabase(database, nonIndexableKeys);
    268             }
    269 
    270             database.setTransactionSuccessful();
    271         } finally {
    272             database.endTransaction();
    273         }
    274     }
    275 
    276     /**
    277      * Inserts {@link SearchIndexableData} into the database.
    278      *
    279      * @param database where the data will be inserted.
    280      * @param localeStr is the locale of the data to be inserted.
    281      * @param dataToUpdate is a {@link List} of the data to be inserted.
    282      * @param nonIndexableKeys is a {@link Map} from Package Name to a {@link Set} of keys which
    283      *                         identify search results which should not be surfaced.
    284      */
    285     @VisibleForTesting
    286     void addDataToDatabase(SQLiteDatabase database, String localeStr,
    287             List<SearchIndexableData> dataToUpdate, Map<String, Set<String>> nonIndexableKeys) {
    288         final long current = System.currentTimeMillis();
    289 
    290         for (SearchIndexableData data : dataToUpdate) {
    291             try {
    292                 indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys);
    293             } catch (Exception e) {
    294                 Log.e(LOG_TAG, "Cannot index: " + (data != null ? data.className : data)
    295                         + " for locale: " + localeStr, e);
    296             }
    297         }
    298 
    299         final long now = System.currentTimeMillis();
    300         Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
    301                 (now - current) + " millis");
    302     }
    303 
    304     /**
    305      * Upholds the validity of enabled data for the user.
    306      * All rows which are enabled but are now flagged with non-indexable keys will become disabled.
    307      * All rows which are disabled but no longer a non-indexable key will become enabled.
    308      *
    309      * @param database The database to validate.
    310      * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it.
    311      */
    312     @VisibleForTesting
    313     void updateDataInDatabase(SQLiteDatabase database,
    314             Map<String, Set<String>> nonIndexableKeys) {
    315         final String whereEnabled = ENABLED + " = 1";
    316         final String whereDisabled = ENABLED + " = 0";
    317 
    318         final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
    319                 whereEnabled, null, null, null, null);
    320 
    321         final ContentValues enabledToDisabledValue = new ContentValues();
    322         enabledToDisabledValue.put(ENABLED, 0);
    323 
    324         String packageName;
    325         // TODO Refactor: Move these two loops into one method.
    326         while (enabledResults.moveToNext()) {
    327             // Package name is the key for remote providers.
    328             // If package name is null, the provider is Settings.
    329             packageName = enabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
    330             if (packageName == null) {
    331                 packageName = mContext.getPackageName();
    332             }
    333 
    334             final String key = enabledResults.getString(COLUMN_INDEX_KEY);
    335             final Set<String> packageKeys = nonIndexableKeys.get(packageName);
    336 
    337             // The indexed item is set to Enabled but is now non-indexable
    338             if (packageKeys != null && packageKeys.contains(key)) {
    339                 final String whereClause = DOCID + " = " + enabledResults.getInt(COLUMN_INDEX_ID);
    340                 database.update(TABLE_PREFS_INDEX, enabledToDisabledValue, whereClause, null);
    341             }
    342         }
    343         enabledResults.close();
    344 
    345         final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
    346                 whereDisabled, null, null, null, null);
    347 
    348         final ContentValues disabledToEnabledValue = new ContentValues();
    349         disabledToEnabledValue.put(ENABLED, 1);
    350 
    351         while (disabledResults.moveToNext()) {
    352             // Package name is the key for remote providers.
    353             // If package name is null, the provider is Settings.
    354             packageName = disabledResults.getString(COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE);
    355             if (packageName == null) {
    356                 packageName = mContext.getPackageName();
    357             }
    358 
    359             final String key = disabledResults.getString(COLUMN_INDEX_KEY);
    360             final Set<String> packageKeys = nonIndexableKeys.get(packageName);
    361 
    362             // The indexed item is set to Disabled but is no longer non-indexable.
    363             // We do not enable keys when packageKeys is null because it means the keys came
    364             // from an unrecognized package and therefore should not be surfaced as results.
    365             if (packageKeys != null && !packageKeys.contains(key)) {
    366                 String whereClause = DOCID + " = " + disabledResults.getInt(COLUMN_INDEX_ID);
    367                 database.update(TABLE_PREFS_INDEX, disabledToEnabledValue, whereClause, null);
    368             }
    369         }
    370         disabledResults.close();
    371     }
    372 
    373     @VisibleForTesting
    374     boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
    375         try {
    376             final Context context = mBaseAuthority.equals(authority) ?
    377                     mContext : mContext.createPackageContext(packageName, 0);
    378 
    379             final Uri uriForResources = buildUriForXmlResources(authority);
    380             addIndexablesForXmlResourceUri(context, packageName, uriForResources,
    381                     SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS);
    382 
    383             final Uri uriForRawData = buildUriForRawData(authority);
    384             addIndexablesForRawDataUri(context, packageName, uriForRawData,
    385                     SearchIndexablesContract.INDEXABLES_RAW_COLUMNS);
    386             return true;
    387         } catch (PackageManager.NameNotFoundException e) {
    388             Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
    389                     + Log.getStackTraceString(e));
    390             return false;
    391         }
    392     }
    393 
    394     @VisibleForTesting
    395     void addNonIndexablesKeysFromRemoteProvider(String packageName,
    396             String authority) {
    397         final List<String> keys =
    398                 getNonIndexablesKeysFromRemoteProvider(packageName, authority);
    399         addNonIndexableKeys(packageName, new HashSet<>(keys));
    400     }
    401 
    402     private List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
    403             String authority) {
    404         try {
    405             final Context packageContext = mContext.createPackageContext(packageName, 0);
    406 
    407             final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
    408             return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
    409                     SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
    410         } catch (PackageManager.NameNotFoundException e) {
    411             Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
    412                     + Log.getStackTraceString(e));
    413             return EMPTY_LIST;
    414         }
    415     }
    416 
    417     private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
    418             String[] projection) {
    419 
    420         final ContentResolver resolver = packageContext.getContentResolver();
    421         final Cursor cursor = resolver.query(uri, projection, null, null, null);
    422 
    423         if (cursor == null) {
    424             Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
    425             return EMPTY_LIST;
    426         }
    427 
    428         final List<String> result = new ArrayList<>();
    429         try {
    430             final int count = cursor.getCount();
    431             if (count > 0) {
    432                 while (cursor.moveToNext()) {
    433                     final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
    434 
    435                     if (TextUtils.isEmpty(key) && Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
    436                         Log.v(LOG_TAG, "Empty non-indexable key from: "
    437                                 + packageContext.getPackageName());
    438                         continue;
    439                     }
    440 
    441                     result.add(key);
    442                 }
    443             }
    444             return result;
    445         } finally {
    446             cursor.close();
    447         }
    448     }
    449 
    450     public void addIndexableData(SearchIndexableData data) {
    451         synchronized (mDataToProcess) {
    452             mDataToProcess.dataToUpdate.add(data);
    453         }
    454     }
    455 
    456     public void addNonIndexableKeys(String authority, Set<String> keys) {
    457         synchronized (mDataToProcess) {
    458             mDataToProcess.nonIndexableKeys.put(authority, keys);
    459         }
    460     }
    461 
    462     /**
    463      * Update the Index for a specific class name resources
    464      *
    465      * @param className              the class name (typically a fragment name).
    466      * @param includeInSearchResults true means that you want the bit "enabled" set so that the
    467      *                               data will be seen included into the search results
    468      */
    469     public void updateFromClassNameResource(String className, boolean includeInSearchResults) {
    470         if (className == null) {
    471             throw new IllegalArgumentException("class name cannot be null!");
    472         }
    473         final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className);
    474         if (res == null) {
    475             Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className);
    476             return;
    477         }
    478         res.context = mContext;
    479         res.enabled = includeInSearchResults;
    480         AsyncTask.execute(new Runnable() {
    481             @Override
    482             public void run() {
    483                 addIndexableData(res);
    484                 updateDatabase(false, Locale.getDefault().toString());
    485                 res.enabled = false;
    486             }
    487         });
    488     }
    489 
    490     private SQLiteDatabase getWritableDatabase() {
    491         try {
    492             return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
    493         } catch (SQLiteException e) {
    494             Log.e(LOG_TAG, "Cannot open writable database", e);
    495             return null;
    496         }
    497     }
    498 
    499     private static Uri buildUriForXmlResources(String authority) {
    500         return Uri.parse("content://" + authority + "/" +
    501                 SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
    502     }
    503 
    504     private static Uri buildUriForRawData(String authority) {
    505         return Uri.parse("content://" + authority + "/" +
    506                 SearchIndexablesContract.INDEXABLES_RAW_PATH);
    507     }
    508 
    509     private static Uri buildUriForNonIndexableKeys(String authority) {
    510         return Uri.parse("content://" + authority + "/" +
    511                 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
    512     }
    513 
    514     private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
    515             Uri uri, String[] projection) {
    516 
    517         final ContentResolver resolver = packageContext.getContentResolver();
    518         final Cursor cursor = resolver.query(uri, projection, null, null, null);
    519 
    520         if (cursor == null) {
    521             Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
    522             return;
    523         }
    524 
    525         try {
    526             final int count = cursor.getCount();
    527             if (count > 0) {
    528                 while (cursor.moveToNext()) {
    529                     final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
    530 
    531                     final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
    532                     final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
    533 
    534                     final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
    535                     final String targetPackage = cursor.getString(
    536                             COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
    537                     final String targetClass = cursor.getString(
    538                             COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
    539 
    540                     SearchIndexableResource sir = new SearchIndexableResource(packageContext);
    541                     sir.xmlResId = xmlResId;
    542                     sir.className = className;
    543                     sir.packageName = packageName;
    544                     sir.iconResId = iconResId;
    545                     sir.intentAction = action;
    546                     sir.intentTargetPackage = targetPackage;
    547                     sir.intentTargetClass = targetClass;
    548 
    549                     addIndexableData(sir);
    550                 }
    551             }
    552         } finally {
    553             cursor.close();
    554         }
    555     }
    556 
    557     private void addIndexablesForRawDataUri(Context packageContext, String packageName,
    558             Uri uri, String[] projection) {
    559 
    560         final ContentResolver resolver = packageContext.getContentResolver();
    561         final Cursor cursor = resolver.query(uri, projection, null, null, null);
    562 
    563         if (cursor == null) {
    564             Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
    565             return;
    566         }
    567 
    568         try {
    569             final int count = cursor.getCount();
    570             if (count > 0) {
    571                 while (cursor.moveToNext()) {
    572                     final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK);
    573                     // TODO Remove rank
    574                     final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
    575                     final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
    576                     final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
    577                     final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
    578                     final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
    579 
    580                     final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
    581 
    582                     final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
    583                     final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
    584 
    585                     final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
    586                     final String targetPackage = cursor.getString(
    587                             COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
    588                     final String targetClass = cursor.getString(
    589                             COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
    590 
    591                     final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
    592                     final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
    593 
    594                     SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
    595                     data.title = title;
    596                     data.summaryOn = summaryOn;
    597                     data.summaryOff = summaryOff;
    598                     data.entries = entries;
    599                     data.keywords = keywords;
    600                     data.screenTitle = screenTitle;
    601                     data.className = className;
    602                     data.packageName = packageName;
    603                     data.iconResId = iconResId;
    604                     data.intentAction = action;
    605                     data.intentTargetPackage = targetPackage;
    606                     data.intentTargetClass = targetClass;
    607                     data.key = key;
    608                     data.userId = userId;
    609 
    610                     addIndexableData(data);
    611                 }
    612             }
    613         } finally {
    614             cursor.close();
    615         }
    616     }
    617 
    618     public void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
    619             SearchIndexableData data, Map<String, Set<String>> nonIndexableKeys) {
    620         if (data instanceof SearchIndexableResource) {
    621             indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
    622         } else if (data instanceof SearchIndexableRaw) {
    623             indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
    624         }
    625     }
    626 
    627     private void indexOneRaw(SQLiteDatabase database, String localeStr,
    628             SearchIndexableRaw raw) {
    629         // Should be the same locale as the one we are processing
    630         if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
    631             return;
    632         }
    633 
    634         DatabaseRow.Builder builder = new DatabaseRow.Builder();
    635         builder.setLocale(localeStr)
    636                 .setEntries(raw.entries)
    637                 .setClassName(raw.className)
    638                 .setScreenTitle(raw.screenTitle)
    639                 .setIconResId(raw.iconResId)
    640                 .setRank(raw.rank)
    641                 .setIntentAction(raw.intentAction)
    642                 .setIntentTargetPackage(raw.intentTargetPackage)
    643                 .setIntentTargetClass(raw.intentTargetClass)
    644                 .setEnabled(raw.enabled)
    645                 .setKey(raw.key)
    646                 .setUserId(raw.userId);
    647 
    648         updateOneRowWithFilteredData(database, builder, raw.title, raw.summaryOn, raw.summaryOff,
    649                 raw.keywords);
    650     }
    651 
    652     private void indexOneResource(SQLiteDatabase database, String localeStr,
    653             SearchIndexableResource sir, Map<String, Set<String>> nonIndexableKeysFromResource) {
    654 
    655         if (sir == null) {
    656             Log.e(LOG_TAG, "Cannot index a null resource!");
    657             return;
    658         }
    659 
    660         final List<String> nonIndexableKeys = new ArrayList<String>();
    661 
    662         if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) {
    663             Set<String> resNonIndexableKeys = nonIndexableKeysFromResource.get(sir.packageName);
    664             if (resNonIndexableKeys != null && resNonIndexableKeys.size() > 0) {
    665                 nonIndexableKeys.addAll(resNonIndexableKeys);
    666             }
    667 
    668             indexFromResource(database, localeStr, sir, nonIndexableKeys);
    669         } else {
    670             if (TextUtils.isEmpty(sir.className)) {
    671                 Log.w(LOG_TAG, "Cannot index an empty Search Provider name!");
    672                 return;
    673             }
    674 
    675             final Class<?> clazz = DatabaseIndexingUtils.getIndexableClass(sir.className);
    676             if (clazz == null) {
    677                 Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className +
    678                         "' should implement the " + Indexable.class.getName() + " interface!");
    679                 return;
    680             }
    681 
    682             // Will be non null only for a Local provider implementing a
    683             // SEARCH_INDEX_DATA_PROVIDER field
    684             final Indexable.SearchIndexProvider provider =
    685                     DatabaseIndexingUtils.getSearchIndexProvider(clazz);
    686             if (provider != null) {
    687                 List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context);
    688                 if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) {
    689                     nonIndexableKeys.addAll(providerNonIndexableKeys);
    690                 }
    691 
    692                 indexFromProvider(database, localeStr, provider, sir, nonIndexableKeys);
    693             }
    694         }
    695     }
    696 
    697     @VisibleForTesting
    698     void indexFromResource(SQLiteDatabase database, String localeStr,
    699             SearchIndexableResource sir, List<String> nonIndexableKeys) {
    700         final Context context = sir.context;
    701         XmlResourceParser parser = null;
    702         try {
    703             parser = context.getResources().getXml(sir.xmlResId);
    704 
    705             int type;
    706             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    707                     && type != XmlPullParser.START_TAG) {
    708                 // Parse next until start tag is found
    709             }
    710 
    711             String nodeName = parser.getName();
    712             if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
    713                 throw new RuntimeException(
    714                         "XML document must start with <PreferenceScreen> tag; found"
    715                                 + nodeName + " at " + parser.getPositionDescription());
    716             }
    717 
    718             final int outerDepth = parser.getDepth();
    719             final AttributeSet attrs = Xml.asAttributeSet(parser);
    720 
    721             final String screenTitle = XmlParserUtils.getDataTitle(context, attrs);
    722             String key = XmlParserUtils.getDataKey(context, attrs);
    723 
    724             String title;
    725             String headerTitle;
    726             String summary;
    727             String headerSummary;
    728             String keywords;
    729             String headerKeywords;
    730             String childFragment;
    731             @DrawableRes
    732             int iconResId;
    733             ResultPayload payload;
    734             boolean enabled;
    735             final String fragmentName = sir.className;
    736             final int rank = sir.rank;
    737             final String intentAction = sir.intentAction;
    738             final String intentTargetPackage = sir.intentTargetPackage;
    739             final String intentTargetClass = sir.intentTargetClass;
    740 
    741             Map<String, PreferenceController> controllerUriMap = null;
    742 
    743             if (fragmentName != null) {
    744                 controllerUriMap = DatabaseIndexingUtils
    745                         .getPreferenceControllerUriMap(fragmentName, context);
    746             }
    747 
    748             // Insert rows for the main PreferenceScreen node. Rewrite the data for removing
    749             // hyphens.
    750 
    751             headerTitle = XmlParserUtils.getDataTitle(context, attrs);
    752             headerSummary = XmlParserUtils.getDataSummary(context, attrs);
    753             headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
    754             enabled = !nonIndexableKeys.contains(key);
    755 
    756             // TODO: Set payload type for header results
    757             DatabaseRow.Builder headerBuilder = new DatabaseRow.Builder();
    758             headerBuilder.setLocale(localeStr)
    759                     .setEntries(null)
    760                     .setClassName(fragmentName)
    761                     .setScreenTitle(screenTitle)
    762                     .setRank(rank)
    763                     .setIntentAction(intentAction)
    764                     .setIntentTargetPackage(intentTargetPackage)
    765                     .setIntentTargetClass(intentTargetClass)
    766                     .setEnabled(enabled)
    767                     .setKey(key)
    768                     .setUserId(-1 /* default user id */);
    769 
    770             // Flag for XML headers which a child element's title.
    771             boolean isHeaderUnique = true;
    772             DatabaseRow.Builder builder;
    773 
    774             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    775                     && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
    776                 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
    777                     continue;
    778                 }
    779 
    780                 nodeName = parser.getName();
    781 
    782                 title = XmlParserUtils.getDataTitle(context, attrs);
    783                 key = XmlParserUtils.getDataKey(context, attrs);
    784                 enabled = ! nonIndexableKeys.contains(key);
    785                 keywords = XmlParserUtils.getDataKeywords(context, attrs);
    786                 iconResId = XmlParserUtils.getDataIcon(context, attrs);
    787 
    788                 if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
    789                     isHeaderUnique = false;
    790                 }
    791 
    792                 builder = new DatabaseRow.Builder();
    793                 builder.setLocale(localeStr)
    794                         .setClassName(fragmentName)
    795                         .setScreenTitle(screenTitle)
    796                         .setIconResId(iconResId)
    797                         .setRank(rank)
    798                         .setIntentAction(intentAction)
    799                         .setIntentTargetPackage(intentTargetPackage)
    800                         .setIntentTargetClass(intentTargetClass)
    801                         .setEnabled(enabled)
    802                         .setKey(key)
    803                         .setUserId(-1 /* default user id */);
    804 
    805                 if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
    806                     summary = XmlParserUtils.getDataSummary(context, attrs);
    807 
    808                     String entries = null;
    809 
    810                     if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
    811                         entries = XmlParserUtils.getDataEntries(context, attrs);
    812                     }
    813 
    814                     payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
    815                     childFragment = XmlParserUtils.getDataChildFragment(context, attrs);
    816 
    817                     builder.setEntries(entries)
    818                             .setChildClassName(childFragment)
    819                             .setPayload(payload);
    820 
    821                     // Insert rows for the child nodes of PreferenceScreen
    822                     updateOneRowWithFilteredData(database, builder, title, summary,
    823                             null /* summary off */, keywords);
    824                 } else {
    825                     String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);
    826                     String summaryOff = XmlParserUtils.getDataSummaryOff(context, attrs);
    827 
    828                     if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) {
    829                         summaryOn = XmlParserUtils.getDataSummary(context, attrs);
    830                     }
    831 
    832                     updateOneRowWithFilteredData(database, builder, title, summaryOn, summaryOff,
    833                             keywords);
    834                 }
    835             }
    836 
    837             // The xml header's title does not match the title of one of the child settings.
    838             if (isHeaderUnique) {
    839                 updateOneRowWithFilteredData(database, headerBuilder, headerTitle, headerSummary,
    840                         null /* summary off */, headerKeywords);
    841             }
    842         } catch (XmlPullParserException e) {
    843             throw new RuntimeException("Error parsing PreferenceScreen", e);
    844         } catch (IOException e) {
    845             throw new RuntimeException("Error parsing PreferenceScreen", e);
    846         } finally {
    847             if (parser != null) parser.close();
    848         }
    849     }
    850 
    851     private void indexFromProvider(SQLiteDatabase database, String localeStr,
    852             Indexable.SearchIndexProvider provider, SearchIndexableResource sir,
    853             List<String> nonIndexableKeys) {
    854 
    855         final String className = sir.className;
    856         final int rank = sir.rank;
    857 
    858         if (provider == null) {
    859             Log.w(LOG_TAG, "Cannot find provider: " + className);
    860             return;
    861         }
    862 
    863         final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(mContext,
    864                 true /* enabled */);
    865 
    866         if (rawList != null) {
    867 
    868             final int rawSize = rawList.size();
    869             for (int i = 0; i < rawSize; i++) {
    870                 SearchIndexableRaw raw = rawList.get(i);
    871 
    872                 // Should be the same locale as the one we are processing
    873                 if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
    874                     continue;
    875                 }
    876                 boolean enabled = !nonIndexableKeys.contains(raw.key);
    877 
    878                 DatabaseRow.Builder builder = new DatabaseRow.Builder();
    879                 builder.setLocale(localeStr)
    880                         .setEntries(raw.entries)
    881                         .setClassName(className)
    882                         .setScreenTitle(raw.screenTitle)
    883                         .setIconResId(raw.iconResId)
    884                         .setRank(rank)
    885                         .setIntentAction(raw.intentAction)
    886                         .setIntentTargetPackage(raw.intentTargetPackage)
    887                         .setIntentTargetClass(raw.intentTargetClass)
    888                         .setEnabled(enabled)
    889                         .setKey(raw.key)
    890                         .setUserId(raw.userId);
    891 
    892                 updateOneRowWithFilteredData(database, builder, raw.title, raw.summaryOn,
    893                         raw.summaryOff, raw.keywords);
    894             }
    895         }
    896 
    897         final List<SearchIndexableResource> resList =
    898                 provider.getXmlResourcesToIndex(mContext, true);
    899         if (resList != null) {
    900             final int resSize = resList.size();
    901             for (int i = 0; i < resSize; i++) {
    902                 SearchIndexableResource item = resList.get(i);
    903 
    904                 // Should be the same locale as the one we are processing
    905                 if (!item.locale.toString().equalsIgnoreCase(localeStr)) {
    906                     continue;
    907                 }
    908 
    909                 item.className = (TextUtils.isEmpty(item.className)) ? className : item.className;
    910 
    911                 indexFromResource(database, localeStr, item, nonIndexableKeys);
    912             }
    913         }
    914     }
    915 
    916     private void updateOneRowWithFilteredData(SQLiteDatabase database, DatabaseRow.Builder builder,
    917             String title, String summaryOn, String summaryOff, String keywords) {
    918 
    919         final String updatedTitle = DatabaseIndexingUtils.normalizeHyphen(title);
    920         final String updatedSummaryOn = DatabaseIndexingUtils.normalizeHyphen(summaryOn);
    921         final String updatedSummaryOff = DatabaseIndexingUtils.normalizeHyphen(summaryOff);
    922 
    923         final String normalizedTitle = DatabaseIndexingUtils.normalizeString(updatedTitle);
    924         final String normalizedSummaryOn = DatabaseIndexingUtils.normalizeString(updatedSummaryOn);
    925         final String normalizedSummaryOff = DatabaseIndexingUtils
    926                 .normalizeString(updatedSummaryOff);
    927 
    928         final String spaceDelimitedKeywords = DatabaseIndexingUtils.normalizeKeywords(keywords);
    929 
    930         builder.setUpdatedTitle(updatedTitle)
    931                 .setUpdatedSummaryOn(updatedSummaryOn)
    932                 .setUpdatedSummaryOff(updatedSummaryOff)
    933                 .setNormalizedTitle(normalizedTitle)
    934                 .setNormalizedSummaryOn(normalizedSummaryOn)
    935                 .setNormalizedSummaryOff(normalizedSummaryOff)
    936                 .setSpaceDelimitedKeywords(spaceDelimitedKeywords);
    937 
    938         updateOneRow(database, builder.build());
    939     }
    940 
    941     private void updateOneRow(SQLiteDatabase database, DatabaseRow row) {
    942 
    943         if (TextUtils.isEmpty(row.updatedTitle)) {
    944             return;
    945         }
    946 
    947         ContentValues values = new ContentValues();
    948         values.put(IndexDatabaseHelper.IndexColumns.DOCID, row.getDocId());
    949         values.put(LOCALE, row.locale);
    950         values.put(DATA_RANK, row.rank);
    951         values.put(DATA_TITLE, row.updatedTitle);
    952         values.put(DATA_TITLE_NORMALIZED, row.normalizedTitle);
    953         values.put(DATA_SUMMARY_ON, row.updatedSummaryOn);
    954         values.put(DATA_SUMMARY_ON_NORMALIZED, row.normalizedSummaryOn);
    955         values.put(DATA_SUMMARY_OFF, row.updatedSummaryOff);
    956         values.put(DATA_SUMMARY_OFF_NORMALIZED, row.normalizedSummaryOff);
    957         values.put(DATA_ENTRIES, row.entries);
    958         values.put(DATA_KEYWORDS, row.spaceDelimitedKeywords);
    959         values.put(CLASS_NAME, row.className);
    960         values.put(SCREEN_TITLE, row.screenTitle);
    961         values.put(INTENT_ACTION, row.intentAction);
    962         values.put(INTENT_TARGET_PACKAGE, row.intentTargetPackage);
    963         values.put(INTENT_TARGET_CLASS, row.intentTargetClass);
    964         values.put(ICON, row.iconResId);
    965         values.put(ENABLED, row.enabled);
    966         values.put(DATA_KEY_REF, row.key);
    967         values.put(USER_ID, row.userId);
    968         values.put(PAYLOAD_TYPE, row.payloadType);
    969         values.put(PAYLOAD, row.payload);
    970 
    971         database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
    972 
    973         if (!TextUtils.isEmpty(row.className) && !TextUtils.isEmpty(row.childClassName)) {
    974             ContentValues siteMapPair = new ContentValues();
    975             final int pairDocId = Objects.hash(row.className, row.childClassName);
    976             siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.DOCID, pairDocId);
    977             siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.PARENT_CLASS, row.className);
    978             siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.PARENT_TITLE, row.screenTitle);
    979             siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.CHILD_CLASS, row.childClassName);
    980             siteMapPair.put(IndexDatabaseHelper.SiteMapColumns.CHILD_TITLE, row.updatedTitle);
    981 
    982             database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, null, siteMapPair);
    983         }
    984     }
    985 
    986     /**
    987      * A private class to describe the indexDatabase data for the Index database
    988      */
    989     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    990     static class UpdateData {
    991         public List<SearchIndexableData> dataToUpdate;
    992         public List<SearchIndexableData> dataToDisable;
    993         public Map<String, Set<String>> nonIndexableKeys;
    994 
    995         public UpdateData() {
    996             dataToUpdate = new ArrayList<>();
    997             dataToDisable = new ArrayList<>();
    998             nonIndexableKeys = new HashMap<>();
    999         }
   1000 
   1001         public UpdateData(UpdateData other) {
   1002             dataToUpdate = new ArrayList<>(other.dataToUpdate);
   1003             dataToDisable = new ArrayList<>(other.dataToDisable);
   1004             nonIndexableKeys = new HashMap<>(other.nonIndexableKeys);
   1005         }
   1006 
   1007         public UpdateData copy() {
   1008             return new UpdateData(this);
   1009         }
   1010 
   1011         public void clear() {
   1012             dataToUpdate.clear();
   1013             dataToDisable.clear();
   1014             nonIndexableKeys.clear();
   1015         }
   1016     }
   1017 
   1018     public static class DatabaseRow {
   1019         public final String locale;
   1020         public final String updatedTitle;
   1021         public final String normalizedTitle;
   1022         public final String updatedSummaryOn;
   1023         public final String normalizedSummaryOn;
   1024         public final String updatedSummaryOff;
   1025         public final String normalizedSummaryOff;
   1026         public final String entries;
   1027         public final String className;
   1028         public final String childClassName;
   1029         public final String screenTitle;
   1030         public final int iconResId;
   1031         public final int rank;
   1032         public final String spaceDelimitedKeywords;
   1033         public final String intentAction;
   1034         public final String intentTargetPackage;
   1035         public final String intentTargetClass;
   1036         public final boolean enabled;
   1037         public final String key;
   1038         public final int userId;
   1039         public final int payloadType;
   1040         public final byte[] payload;
   1041 
   1042         private DatabaseRow(Builder builder) {
   1043             locale = builder.mLocale;
   1044             updatedTitle = builder.mUpdatedTitle;
   1045             normalizedTitle = builder.mNormalizedTitle;
   1046             updatedSummaryOn = builder.mUpdatedSummaryOn;
   1047             normalizedSummaryOn = builder.mNormalizedSummaryOn;
   1048             updatedSummaryOff = builder.mUpdatedSummaryOff;
   1049             normalizedSummaryOff = builder.mNormalizedSummaryOff;
   1050             entries = builder.mEntries;
   1051             className = builder.mClassName;
   1052             childClassName = builder.mChildClassName;
   1053             screenTitle = builder.mScreenTitle;
   1054             iconResId = builder.mIconResId;
   1055             rank = builder.mRank;
   1056             spaceDelimitedKeywords = builder.mSpaceDelimitedKeywords;
   1057             intentAction = builder.mIntentAction;
   1058             intentTargetPackage = builder.mIntentTargetPackage;
   1059             intentTargetClass = builder.mIntentTargetClass;
   1060             enabled = builder.mEnabled;
   1061             key = builder.mKey;
   1062             userId = builder.mUserId;
   1063             payloadType = builder.mPayloadType;
   1064             payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload)
   1065                     : null;
   1066         }
   1067 
   1068         /**
   1069          * Returns the doc id for this row.
   1070          */
   1071         public int getDocId() {
   1072             // The DocID should contains more than the title string itself (you may have two
   1073             // settings with the same title). So we need to use a combination of multiple
   1074             // attributes from this row.
   1075             return Objects.hash(updatedTitle, screenTitle, key, payloadType);
   1076         }
   1077 
   1078         public static class Builder {
   1079             private String mLocale;
   1080             private String mUpdatedTitle;
   1081             private String mNormalizedTitle;
   1082             private String mUpdatedSummaryOn;
   1083             private String mNormalizedSummaryOn;
   1084             private String mUpdatedSummaryOff;
   1085             private String mNormalizedSummaryOff;
   1086             private String mEntries;
   1087             private String mClassName;
   1088             private String mChildClassName;
   1089             private String mScreenTitle;
   1090             private int mIconResId;
   1091             private int mRank;
   1092             private String mSpaceDelimitedKeywords;
   1093             private String mIntentAction;
   1094             private String mIntentTargetPackage;
   1095             private String mIntentTargetClass;
   1096             private boolean mEnabled;
   1097             private String mKey;
   1098             private int mUserId;
   1099             @ResultPayload.PayloadType
   1100             private int mPayloadType;
   1101             private ResultPayload mPayload;
   1102 
   1103             public Builder setLocale(String locale) {
   1104                 mLocale = locale;
   1105                 return this;
   1106             }
   1107 
   1108             public Builder setUpdatedTitle(String updatedTitle) {
   1109                 mUpdatedTitle = updatedTitle;
   1110                 return this;
   1111             }
   1112 
   1113             public Builder setNormalizedTitle(String normalizedTitle) {
   1114                 mNormalizedTitle = normalizedTitle;
   1115                 return this;
   1116             }
   1117 
   1118             public Builder setUpdatedSummaryOn(String updatedSummaryOn) {
   1119                 mUpdatedSummaryOn = updatedSummaryOn;
   1120                 return this;
   1121             }
   1122 
   1123             public Builder setNormalizedSummaryOn(String normalizedSummaryOn) {
   1124                 mNormalizedSummaryOn = normalizedSummaryOn;
   1125                 return this;
   1126             }
   1127 
   1128             public Builder setUpdatedSummaryOff(String updatedSummaryOff) {
   1129                 mUpdatedSummaryOff = updatedSummaryOff;
   1130                 return this;
   1131             }
   1132 
   1133             public Builder setNormalizedSummaryOff(String normalizedSummaryOff) {
   1134                 this.mNormalizedSummaryOff = normalizedSummaryOff;
   1135                 return this;
   1136             }
   1137 
   1138             public Builder setEntries(String entries) {
   1139                 mEntries = entries;
   1140                 return this;
   1141             }
   1142 
   1143             public Builder setClassName(String className) {
   1144                 mClassName = className;
   1145                 return this;
   1146             }
   1147 
   1148             public Builder setChildClassName(String childClassName) {
   1149                 mChildClassName = childClassName;
   1150                 return this;
   1151             }
   1152 
   1153             public Builder setScreenTitle(String screenTitle) {
   1154                 mScreenTitle = screenTitle;
   1155                 return this;
   1156             }
   1157 
   1158             public Builder setIconResId(int iconResId) {
   1159                 mIconResId = iconResId;
   1160                 return this;
   1161             }
   1162 
   1163             public Builder setRank(int rank) {
   1164                 mRank = rank;
   1165                 return this;
   1166             }
   1167 
   1168             public Builder setSpaceDelimitedKeywords(String spaceDelimitedKeywords) {
   1169                 mSpaceDelimitedKeywords = spaceDelimitedKeywords;
   1170                 return this;
   1171             }
   1172 
   1173             public Builder setIntentAction(String intentAction) {
   1174                 mIntentAction = intentAction;
   1175                 return this;
   1176             }
   1177 
   1178             public Builder setIntentTargetPackage(String intentTargetPackage) {
   1179                 mIntentTargetPackage = intentTargetPackage;
   1180                 return this;
   1181             }
   1182 
   1183             public Builder setIntentTargetClass(String intentTargetClass) {
   1184                 mIntentTargetClass = intentTargetClass;
   1185                 return this;
   1186             }
   1187 
   1188             public Builder setEnabled(boolean enabled) {
   1189                 mEnabled = enabled;
   1190                 return this;
   1191             }
   1192 
   1193             public Builder setKey(String key) {
   1194                 mKey = key;
   1195                 return this;
   1196             }
   1197 
   1198             public Builder setUserId(int userId) {
   1199                 mUserId = userId;
   1200                 return this;
   1201             }
   1202 
   1203             public Builder setPayload(ResultPayload payload) {
   1204                 mPayload = payload;
   1205 
   1206                 if (mPayload != null) {
   1207                     setPayloadType(mPayload.getType());
   1208                 }
   1209                 return this;
   1210             }
   1211 
   1212             /**
   1213              * Payload type is added when a Payload is added to the Builder in {setPayload}
   1214              *
   1215              * @param payloadType PayloadType
   1216              * @return The Builder
   1217              */
   1218             private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) {
   1219                 mPayloadType = payloadType;
   1220                 return this;
   1221             }
   1222 
   1223             public DatabaseRow build() {
   1224                 return new DatabaseRow(this);
   1225             }
   1226         }
   1227     }
   1228 
   1229     public class IndexingTask extends AsyncTask<Void, Void, Void> {
   1230 
   1231         @VisibleForTesting
   1232         IndexingCallback mCallback;
   1233 
   1234         public IndexingTask(IndexingCallback callback) {
   1235             mCallback = callback;
   1236         }
   1237 
   1238         @Override
   1239         protected void onPreExecute() {
   1240             mIsIndexingComplete.set(false);
   1241         }
   1242 
   1243         @Override
   1244         protected Void doInBackground(Void... voids) {
   1245             performIndexing();
   1246             return null;
   1247         }
   1248 
   1249         @Override
   1250         protected void onPostExecute(Void aVoid) {
   1251             mIsIndexingComplete.set(true);
   1252             if (mCallback != null) {
   1253                 mCallback.onIndexingFinished();
   1254             }
   1255         }
   1256     }
   1257 }