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