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