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