2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 package com.android.settings.search;
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;
45 import com.android.internal.logging.nano.MetricsProto;
46 import com.android.settings.SettingsActivity;
47 import com.android.settings.core.PreferenceController;
49 import com.android.settings.overlay.FeatureFactory;
50 import org.xmlpull.v1.XmlPullParser;
51 import org.xmlpull.v1.XmlPullParserException;
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;
61 import java.util.Objects;
63 import java.util.concurrent.atomic.AtomicBoolean;
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;
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;
113 * Consumes the SearchIndexableProvider content providers.
114 * Updates the Resource, Raw Data and non-indexable data for Search.
116 * TODO this class needs to be refactored by moving most of its methods into controllers
118 public class DatabaseIndexingManager {
119 private static final String LOG_TAG = "DatabaseIndexingManager";
121 private static final String METRICS_ACTION_SETTINGS_ASYNC_INDEX =
122 "search_asynchronous_indexing";
124 public static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER =
125 "SEARCH_INDEX_DATA_PROVIDER";
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";
131 private static final List<String> EMPTY_LIST = Collections.emptyList();
133 private final String mBaseAuthority;
136 final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false);
139 final UpdateData mDataToProcess = new UpdateData();
140 private Context mContext;
142 public DatabaseIndexingManager(Context context, String baseAuthority) {
144 mBaseAuthority = baseAuthority;
147 public void setContext(Context context) {
151 public boolean isIndexingComplete() {
152 return mIsIndexingComplete.get();
155 public void indexDatabase(IndexingCallback callback) {
156 IndexingTask task = new IndexingTask(callback);
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.
165 public void performIndexing() {
166 final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE);
167 final List<ResolveInfo> list =
168 mContext.getPackageManager().queryIntentContentProviders(intent, 0);
170 String localeStr = Locale.getDefault().toString();
171 String fingerprint = Build.FINGERPRINT;
172 final boolean isFullIndex = isFullIndex(localeStr, fingerprint);
178 for (final ResolveInfo info : list) {
179 if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) {
182 final String authority = info.providerInfo.authority;
183 final String packageName = info.providerInfo.packageName;
186 addIndexablesFromRemoteProvider(packageName, authority);
188 addNonIndexablesKeysFromRemoteProvider(packageName, authority);
191 updateDatabase(isFullIndex, localeStr);
193 IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr);
194 IndexDatabaseHelper.setBuildIndexed(mContext, fingerprint);
198 * Perform a full index on an OTA or when the locale has changed
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.
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;
214 * Reconstruct the database in the following cases:
215 * - Language has changed
216 * - Build has changed
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);
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.
232 * @param needsReindexing true the database needs to be rebuilt.
233 * @param localeStr the default locale for the device.
236 void updateDatabase(boolean needsReindexing, String localeStr) {
237 final UpdateData copy;
239 synchronized (mDataToProcess) {
240 copy = mDataToProcess.copy();
241 mDataToProcess.clear();
244 final List<SearchIndexableData> dataToUpdate = copy.dataToUpdate;
245 final Map<String, Set<String>> nonIndexableKeys = copy.nonIndexableKeys;
247 final SQLiteDatabase database = getWritableDatabase();
248 if (database == null) {
249 Log.w(LOG_TAG, "Cannot indexDatabase Index as I cannot get a writable database");
254 database.beginTransaction();
256 // Add new data from Providers at initial index time, or inserted later.
257 if (dataToUpdate.size() > 0) {
258 addDataToDatabase(database, localeStr, dataToUpdate, nonIndexableKeys);
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);
267 database.setTransactionSuccessful();
269 database.endTransaction();
274 * Inserts {@link SearchIndexableData} into the database.
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.
283 void addDataToDatabase(SQLiteDatabase database, String localeStr,
284 List<SearchIndexableData> dataToUpdate, Map<String, Set<String>> nonIndexableKeys) {
285 final long current = System.currentTimeMillis();
287 for (SearchIndexableData data : dataToUpdate) {
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);
296 final long now = System.currentTimeMillis();
297 Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
298 (now - current) + " millis");
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.
306 * @param database The database to validate.
307 * @param nonIndexableKeys A map between package name and the set of non-indexable keys for it.
310 void updateDataInDatabase(SQLiteDatabase database,
311 Map<String, Set<String>> nonIndexableKeys) {
312 final String whereEnabled = ENABLED + " = 1";
313 final String whereDisabled = ENABLED + " = 0";
315 final Cursor enabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
316 whereEnabled, null, null, null, null);
318 final ContentValues enabledToDisabledValue = new ContentValues();
319 enabledToDisabledValue.put(ENABLED, 0);
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();
331 final String key = enabledResults.getString(COLUMN_INDEX_KEY);
332 final Set<String> packageKeys = nonIndexableKeys.get(packageName);
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);
340 enabledResults.close();
342 final Cursor disabledResults = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
343 whereDisabled, null, null, null, null);
345 final ContentValues disabledToEnabledValue = new ContentValues();
346 disabledToEnabledValue.put(ENABLED, 1);
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();
356 final String key = disabledResults.getString(COLUMN_INDEX_KEY);
357 final Set<String> packageKeys = nonIndexableKeys.get(packageName);
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);
367 disabledResults.close();
371 boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
373 final Context context = mBaseAuthority.equals(authority) ?
374 mContext : mContext.createPackageContext(packageName, 0);
376 final Uri uriForResources = buildUriForXmlResources(authority);
377 addIndexablesForXmlResourceUri(context, packageName, uriForResources,
378 SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS);
380 final Uri uriForRawData = buildUriForRawData(authority);
381 addIndexablesForRawDataUri(context, packageName, uriForRawData,
382 SearchIndexablesContract.INDEXABLES_RAW_COLUMNS);
384 } catch (PackageManager.NameNotFoundException e) {
385 Log.w(LOG_TAG, "Could not create context for " + packageName + ": "
386 + Log.getStackTraceString(e));
392 void addNonIndexablesKeysFromRemoteProvider(String packageName,
394 final List<String> keys =
395 getNonIndexablesKeysFromRemoteProvider(packageName, authority);
396 addNonIndexableKeys(packageName, new HashSet<>(keys));
399 private List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
402 final Context packageContext = mContext.createPackageContext(packageName, 0);
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));
414 private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
415 String[] projection) {
417 final ContentResolver resolver = packageContext.getContentResolver();
418 final Cursor cursor = resolver.query(uri, projection, null, null, null);
420 if (cursor == null) {
421 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
425 final List<String> result = new ArrayList<>();
427 final int count = cursor.getCount();
429 while (cursor.moveToNext()) {
430 final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
432 if (TextUtils.isEmpty(key) && Log.isLoggable(LOG_TAG, Log.VERBOSE)) {
433 Log.v(LOG_TAG, "Empty non-indexable key from: "
434 + packageContext.getPackageName());
447 public void addIndexableData(SearchIndexableData data) {
448 synchronized (mDataToProcess) {
449 mDataToProcess.dataToUpdate.add(data);
453 public void addNonIndexableKeys(String authority, Set<String> keys) {
454 synchronized (mDataToProcess) {
455 mDataToProcess.nonIndexableKeys.put(authority, keys);
460 * Update the Index for a specific class name resources
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
466 public void updateFromClassNameResource(String className, boolean includeInSearchResults) {
467 if (className == null) {
468 throw new IllegalArgumentException("class name cannot be null!");
470 final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className);
472 Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className);
475 res.context = mContext;
476 res.enabled = includeInSearchResults;
477 AsyncTask.execute(new Runnable() {
480 addIndexableData(res);
481 updateDatabase(false, Locale.getDefault().toString());
487 private SQLiteDatabase getWritableDatabase() {
489 return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
490 } catch (SQLiteException e) {
491 Log.e(LOG_TAG, "Cannot open writable database", e);
496 private static Uri buildUriForXmlResources(String authority) {
497 return Uri.parse("content://" + authority + "/" +
498 SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
501 private static Uri buildUriForRawData(String authority) {
502 return Uri.parse("content://" + authority + "/" +
503 SearchIndexablesContract.INDEXABLES_RAW_PATH);
506 private static Uri buildUriForNonIndexableKeys(String authority) {
507 return Uri.parse("content://" + authority + "/" +
508 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
511 private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
512 Uri uri, String[] projection) {
514 final ContentResolver resolver = packageContext.getContentResolver();
515 final Cursor cursor = resolver.query(uri, projection, null, null, null);
517 if (cursor == null) {
518 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
523 final int count = cursor.getCount();
525 while (cursor.moveToNext()) {
526 final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
528 final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
529 final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
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);
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;
546 addIndexableData(sir);
554 private void addIndexablesForRawDataUri(Context packageContext, String packageName,
555 Uri uri, String[] projection) {
557 final ContentResolver resolver = packageContext.getContentResolver();
558 final Cursor cursor = resolver.query(uri, projection, null, null, null);
560 if (cursor == null) {
561 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
566 final int count = cursor.getCount();
568 while (cursor.moveToNext()) {
569 final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_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);
577 final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
579 final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
580 final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
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);
588 final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
589 final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
591 SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
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;
605 data.userId = userId;
607 addIndexableData(data);
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);
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)) {
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)
638 .setIntentAction(raw.intentAction)
639 .setIntentTargetPackage(raw.intentTargetPackage)
640 .setIntentTargetClass(raw.intentTargetClass)
641 .setEnabled(raw.enabled)
643 .setUserId(raw.userId);
645 updateOneRowWithFilteredData(database, builder, raw.title, raw.summaryOn, raw.summaryOff,
649 private void indexOneResource(SQLiteDatabase database, String localeStr,
650 SearchIndexableResource sir, Map<String, Set<String>> nonIndexableKeysFromResource) {
653 Log.e(LOG_TAG, "Cannot index a null resource!");
657 final List<String> nonIndexableKeys = new ArrayList<String>();
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);
665 indexFromResource(database, localeStr, sir, nonIndexableKeys);
667 if (TextUtils.isEmpty(sir.className)) {
668 Log.w(LOG_TAG, "Cannot index an empty Search Provider name!");
672 final Class<?> clazz = DatabaseIndexingUtils.getIndexableClass(sir.className);
674 Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className +
675 "' should implement the " + Indexable.class.getName() + " interface!");
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);
689 indexFromProvider(database, localeStr, provider, sir, nonIndexableKeys);
695 void indexFromResource(SQLiteDatabase database, String localeStr,
696 SearchIndexableResource sir, List<String> nonIndexableKeys) {
697 final Context context = sir.context;
698 XmlResourceParser parser = null;
700 parser = context.getResources().getXml(sir.xmlResId);
703 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
704 && type != XmlPullParser.START_TAG) {
705 // Parse next until start tag is found
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());
715 final int outerDepth = parser.getDepth();
716 final AttributeSet attrs = Xml.asAttributeSet(parser);
718 final String screenTitle = XmlParserUtils.getDataTitle(context, attrs);
719 String key = XmlParserUtils.getDataKey(context, attrs);
724 String headerSummary;
726 String headerKeywords;
727 String childFragment;
730 ResultPayload payload;
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;
738 Map<String, PreferenceController> controllerUriMap = null;
740 if (fragmentName != null) {
741 controllerUriMap = DatabaseIndexingUtils
742 .getPreferenceControllerUriMap(fragmentName, context);
745 // Insert rows for the main PreferenceScreen node. Rewrite the data for removing
748 headerTitle = XmlParserUtils.getDataTitle(context, attrs);
749 headerSummary = XmlParserUtils.getDataSummary(context, attrs);
750 headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
751 enabled = !nonIndexableKeys.contains(key);
753 // TODO: Set payload type for header results
754 DatabaseRow.Builder headerBuilder = new DatabaseRow.Builder();
755 headerBuilder.setLocale(localeStr)
757 .setClassName(fragmentName)
758 .setScreenTitle(screenTitle)
760 .setIntentAction(intentAction)
761 .setIntentTargetPackage(intentTargetPackage)
762 .setIntentTargetClass(intentTargetClass)
765 .setUserId(-1 /* default user id */);
767 // Flag for XML headers which a child element's title.
768 boolean isHeaderUnique = true;
769 DatabaseRow.Builder builder;
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) {
777 nodeName = parser.getName();
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);
785 if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
786 isHeaderUnique = false;
789 builder = new DatabaseRow.Builder();
790 builder.setLocale(localeStr)
791 .setClassName(fragmentName)
792 .setScreenTitle(screenTitle)
793 .setIconResId(iconResId)
795 .setIntentAction(intentAction)
796 .setIntentTargetPackage(intentTargetPackage)
797 .setIntentTargetClass(intentTargetClass)
800 .setUserId(-1 /* default user id */);
802 if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
803 summary = XmlParserUtils.getDataSummary(context, attrs);
805 String entries = null;
807 if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
808 entries = XmlParserUtils.getDataEntries(context, attrs);
811 // TODO (b/62254931) index primitives instead of payload
812 payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
813 childFragment = XmlParserUtils.getDataChildFragment(context, attrs);
815 builder.setEntries(entries)
816 .setChildClassName(childFragment)
817 .setPayload(payload);
819 // Insert rows for the child nodes of PreferenceScreen
820 updateOneRowWithFilteredData(database, builder, title, summary,
821 null /* summary off */, keywords);
823 String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);
824 String summaryOff = XmlParserUtils.getDataSummaryOff(context, attrs);
826 if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) {
827 summaryOn = XmlParserUtils.getDataSummary(context, attrs);
830 updateOneRowWithFilteredData(database, builder, title, summaryOn, summaryOff,
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);
840 } catch (XmlPullParserException e) {
841 throw new RuntimeException("Error parsing PreferenceScreen", e);
842 } catch (IOException e) {
843 throw new RuntimeException("Error parsing PreferenceScreen", e);
845 if (parser != null) parser.close();
849 private void indexFromProvider(SQLiteDatabase database, String localeStr,
850 Indexable.SearchIndexProvider provider, SearchIndexableResource sir,
851 List<String> nonIndexableKeys) {
853 final String className = sir.className;
854 final int rank = sir.rank;
856 if (provider == null) {
857 Log.w(LOG_TAG, "Cannot find provider: " + className);
861 final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(mContext,
864 if (rawList != null) {
866 final int rawSize = rawList.size();
867 for (int i = 0; i < rawSize; i++) {
868 SearchIndexableRaw raw = rawList.get(i);
870 // Should be the same locale as the one we are processing
871 if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
874 boolean enabled = !nonIndexableKeys.contains(raw.key);
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)
883 .setIntentAction(raw.intentAction)
884 .setIntentTargetPackage(raw.intentTargetPackage)
885 .setIntentTargetClass(raw.intentTargetClass)
888 .setUserId(raw.userId);
890 updateOneRowWithFilteredData(database, builder, raw.title, raw.summaryOn,
891 raw.summaryOff, raw.keywords);
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);
902 // Should be the same locale as the one we are processing
903 if (!item.locale.toString().equalsIgnoreCase(localeStr)) {
907 item.className = (TextUtils.isEmpty(item.className)) ? className : item.className;
909 indexFromResource(database, localeStr, item, nonIndexableKeys);
914 private void updateOneRowWithFilteredData(SQLiteDatabase database, DatabaseRow.Builder builder,
915 String title, String summaryOn, String summaryOff, String keywords) {
917 final String updatedTitle = DatabaseIndexingUtils.normalizeHyphen(title);
918 final String updatedSummaryOn = DatabaseIndexingUtils.normalizeHyphen(summaryOn);
919 final String updatedSummaryOff = DatabaseIndexingUtils.normalizeHyphen(summaryOff);
921 final String normalizedTitle = DatabaseIndexingUtils.normalizeString(updatedTitle);
922 final String normalizedSummaryOn = DatabaseIndexingUtils.normalizeString(updatedSummaryOn);
923 final String normalizedSummaryOff = DatabaseIndexingUtils
924 .normalizeString(updatedSummaryOff);
926 final String spaceDelimitedKeywords = DatabaseIndexingUtils.normalizeKeywords(keywords);
928 builder.setUpdatedTitle(updatedTitle)
929 .setUpdatedSummaryOn(updatedSummaryOn)
930 .setUpdatedSummaryOff(updatedSummaryOff)
931 .setNormalizedTitle(normalizedTitle)
932 .setNormalizedSummaryOn(normalizedSummaryOn)
933 .setNormalizedSummaryOff(normalizedSummaryOff)
934 .setSpaceDelimitedKeywords(spaceDelimitedKeywords);
936 updateOneRow(database, builder.build(mContext));
939 private void updateOneRow(SQLiteDatabase database, DatabaseRow row) {
941 if (TextUtils.isEmpty(row.updatedTitle)) {
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);
969 database.replaceOrThrow(TABLE_PREFS_INDEX, null, values);
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);
980 database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_SITE_MAP, null, siteMapPair);
985 * A private class to describe the indexDatabase data for the Index database
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;
993 public UpdateData() {
994 dataToUpdate = new ArrayList<>();
995 dataToDisable = new ArrayList<>();
996 nonIndexableKeys = new HashMap<>();
999 public UpdateData(UpdateData other) {
1000 dataToUpdate = new ArrayList<>(other.dataToUpdate);
1001 dataToDisable = new ArrayList<>(other.dataToDisable);
1002 nonIndexableKeys = new HashMap<>(other.nonIndexableKeys);
1005 public UpdateData copy() {
1006 return new UpdateData(this);
1009 public void clear() {
1010 dataToUpdate.clear();
1011 dataToDisable.clear();
1012 nonIndexableKeys.clear();
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;
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;
1060 userId = builder.mUserId;
1061 payloadType = builder.mPayloadType;
1062 payload = builder.mPayload != null ? ResultPayloadUtils.marshall(builder.mPayload)
1067 * Returns the doc id for this row.
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)
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;
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;
1102 public Builder setLocale(String locale) {
1107 public Builder setUpdatedTitle(String updatedTitle) {
1108 mUpdatedTitle = updatedTitle;
1112 public Builder setNormalizedTitle(String normalizedTitle) {
1113 mNormalizedTitle = normalizedTitle;
1117 public Builder setUpdatedSummaryOn(String updatedSummaryOn) {
1118 mUpdatedSummaryOn = updatedSummaryOn;
1122 public Builder setNormalizedSummaryOn(String normalizedSummaryOn) {
1123 mNormalizedSummaryOn = normalizedSummaryOn;
1127 public Builder setUpdatedSummaryOff(String updatedSummaryOff) {
1128 mUpdatedSummaryOff = updatedSummaryOff;
1132 public Builder setNormalizedSummaryOff(String normalizedSummaryOff) {
1133 this.mNormalizedSummaryOff = normalizedSummaryOff;
1137 public Builder setEntries(String entries) {
1142 public Builder setClassName(String className) {
1143 mClassName = className;
1147 public Builder setChildClassName(String childClassName) {
1148 mChildClassName = childClassName;
1152 public Builder setScreenTitle(String screenTitle) {
1153 mScreenTitle = screenTitle;
1157 public Builder setIconResId(int iconResId) {
1158 mIconResId = iconResId;
1162 public Builder setRank(int rank) {
1167 public Builder setSpaceDelimitedKeywords(String spaceDelimitedKeywords) {
1168 mSpaceDelimitedKeywords = spaceDelimitedKeywords;
1172 public Builder setIntentAction(String intentAction) {
1173 mIntentAction = intentAction;
1177 public Builder setIntentTargetPackage(String intentTargetPackage) {
1178 mIntentTargetPackage = intentTargetPackage;
1182 public Builder setIntentTargetClass(String intentTargetClass) {
1183 mIntentTargetClass = intentTargetClass;
1187 public Builder setEnabled(boolean enabled) {
1192 public Builder setKey(String key) {
1197 public Builder setUserId(int userId) {
1202 public Builder setPayload(ResultPayload payload) {
1205 if (mPayload != null) {
1206 setPayloadType(mPayload.getType());
1212 * Payload type is added when a Payload is added to the Builder in {setPayload}
1214 * @param payloadType PayloadType
1215 * @return The Builder
1217 private Builder setPayloadType(@ResultPayload.PayloadType int payloadType) {
1218 mPayloadType = payloadType;
1223 * Adds intent to inline payloads, or creates an Intent Payload as a fallback if the
1226 private void setIntent(Context context) {
1227 if (mPayload != null) {
1230 final Intent intent = buildIntent(context);
1231 mPayload = new ResultPayload(intent);
1232 mPayloadType = ResultPayload.PayloadType.INTENT;
1236 * Adds Intent payload to builder.
1238 private Intent buildIntent(Context context) {
1239 final Intent intent;
1241 if (TextUtils.isEmpty(mIntentAction)) {
1242 // Action is null, we will launch it as a sub-setting
1243 intent = DatabaseIndexingUtils.buildSubsettingIntent(context, mClassName, mKey,
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,
1252 intent.setComponent(component);
1254 intent.putExtra(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY, mKey);
1259 public DatabaseRow build(Context context) {
1261 return new DatabaseRow(this);
1266 public class IndexingTask extends AsyncTask<Void, Void, Void> {
1269 IndexingCallback mCallback;
1270 private long mIndexStartTime;
1272 public IndexingTask(IndexingCallback callback) {
1273 mCallback = callback;
1277 protected void onPreExecute() {
1278 mIndexStartTime = System.currentTimeMillis();
1279 mIsIndexingComplete.set(false);
1283 protected Void doInBackground(Void... voids) {
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);
1294 mIsIndexingComplete.set(true);
1295 if (mCallback != null) {
1296 mCallback.onIndexingFinished();