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