OSDN Git Service

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