1 /* 2 * Copyright (C) 2014 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 package com.android.settings.search; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.PackageInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.content.res.TypedArray; 28 import android.content.res.XmlResourceParser; 29 import android.database.Cursor; 30 import android.database.DatabaseUtils; 31 import android.database.MergeCursor; 32 import android.database.sqlite.SQLiteDatabase; 33 import android.net.Uri; 34 import android.os.AsyncTask; 35 import android.provider.SearchIndexableData; 36 import android.provider.SearchIndexableResource; 37 import android.provider.SearchIndexablesContract; 38 import android.text.TextUtils; 39 import android.util.AttributeSet; 40 import android.util.Log; 41 import android.util.TypedValue; 42 import android.util.Xml; 43 import com.android.settings.R; 44 import org.xmlpull.v1.XmlPullParser; 45 import org.xmlpull.v1.XmlPullParserException; 46 47 import java.io.IOException; 48 import java.lang.reflect.Field; 49 import java.text.Normalizer; 50 import java.util.ArrayList; 51 import java.util.Collections; 52 import java.util.Date; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Locale; 56 import java.util.Map; 57 import java.util.concurrent.ExecutionException; 58 import java.util.concurrent.atomic.AtomicBoolean; 59 import java.util.regex.Pattern; 60 61 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; 62 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; 63 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; 64 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; 65 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; 66 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; 67 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; 68 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; 69 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; 70 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; 71 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; 72 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; 73 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; 74 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; 75 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; 76 77 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; 78 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; 79 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; 80 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; 81 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; 82 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; 83 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; 84 85 import static com.android.settings.search.IndexDatabaseHelper.Tables; 86 import static com.android.settings.search.IndexDatabaseHelper.IndexColumns; 87 88 public class Index { 89 90 private static final String LOG_TAG = "Index"; 91 92 // Those indices should match the indices of SELECT_COLUMNS ! 93 public static final int COLUMN_INDEX_RANK = 0; 94 public static final int COLUMN_INDEX_TITLE = 1; 95 public static final int COLUMN_INDEX_SUMMARY_ON = 2; 96 public static final int COLUMN_INDEX_SUMMARY_OFF = 3; 97 public static final int COLUMN_INDEX_ENTRIES = 4; 98 public static final int COLUMN_INDEX_KEYWORDS = 5; 99 public static final int COLUMN_INDEX_CLASS_NAME = 6; 100 public static final int COLUMN_INDEX_SCREEN_TITLE = 7; 101 public static final int COLUMN_INDEX_ICON = 8; 102 public static final int COLUMN_INDEX_INTENT_ACTION = 9; 103 public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 10; 104 public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 11; 105 public static final int COLUMN_INDEX_ENABLED = 12; 106 public static final int COLUMN_INDEX_KEY = 13; 107 public static final int COLUMN_INDEX_USER_ID = 14; 108 109 public static final String ENTRIES_SEPARATOR = "|"; 110 111 // If you change the order of columns here, you SHOULD change the COLUMN_INDEX_XXX values 112 private static final String[] SELECT_COLUMNS = new String[] { 113 IndexColumns.DATA_RANK, // 0 114 IndexColumns.DATA_TITLE, // 1 115 IndexColumns.DATA_SUMMARY_ON, // 2 116 IndexColumns.DATA_SUMMARY_OFF, // 3 117 IndexColumns.DATA_ENTRIES, // 4 118 IndexColumns.DATA_KEYWORDS, // 5 119 IndexColumns.CLASS_NAME, // 6 120 IndexColumns.SCREEN_TITLE, // 7 121 IndexColumns.ICON, // 8 122 IndexColumns.INTENT_ACTION, // 9 123 IndexColumns.INTENT_TARGET_PACKAGE, // 10 124 IndexColumns.INTENT_TARGET_CLASS, // 11 125 IndexColumns.ENABLED, // 12 126 IndexColumns.DATA_KEY_REF // 13 127 }; 128 129 private static final String[] MATCH_COLUMNS_PRIMARY = { 130 IndexColumns.DATA_TITLE, 131 IndexColumns.DATA_TITLE_NORMALIZED, 132 IndexColumns.DATA_KEYWORDS 133 }; 134 135 private static final String[] MATCH_COLUMNS_SECONDARY = { 136 IndexColumns.DATA_SUMMARY_ON, 137 IndexColumns.DATA_SUMMARY_ON_NORMALIZED, 138 IndexColumns.DATA_SUMMARY_OFF, 139 IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, 140 IndexColumns.DATA_ENTRIES 141 }; 142 143 // Max number of saved search queries (who will be used for proposing suggestions) 144 private static long MAX_SAVED_SEARCH_QUERY = 64; 145 // Max number of proposed suggestions 146 private static final int MAX_PROPOSED_SUGGESTIONS = 5; 147 148 private static final String BASE_AUTHORITY = "com.android.settings"; 149 150 private static final String EMPTY = ""; 151 private static final String NON_BREAKING_HYPHEN = "\u2011"; 152 private static final String HYPHEN = "-"; 153 154 private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER = 155 "SEARCH_INDEX_DATA_PROVIDER"; 156 157 private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; 158 private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; 159 private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; 160 161 private static final List<String> EMPTY_LIST = Collections.<String>emptyList(); 162 163 private static Index sInstance; 164 165 private static final Pattern REMOVE_DIACRITICALS_PATTERN 166 = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); 167 168 /** 169 * A private class to describe the update data for the Index database 170 */ 171 private static class UpdateData { 172 public List<SearchIndexableData> dataToUpdate; 173 public List<SearchIndexableData> dataToDelete; 174 public Map<String, List<String>> nonIndexableKeys; 175 176 public boolean forceUpdate = false; 177 UpdateData()178 public UpdateData() { 179 dataToUpdate = new ArrayList<SearchIndexableData>(); 180 dataToDelete = new ArrayList<SearchIndexableData>(); 181 nonIndexableKeys = new HashMap<String, List<String>>(); 182 } 183 UpdateData(UpdateData other)184 public UpdateData(UpdateData other) { 185 dataToUpdate = new ArrayList<SearchIndexableData>(other.dataToUpdate); 186 dataToDelete = new ArrayList<SearchIndexableData>(other.dataToDelete); 187 nonIndexableKeys = new HashMap<String, List<String>>(other.nonIndexableKeys); 188 forceUpdate = other.forceUpdate; 189 } 190 copy()191 public UpdateData copy() { 192 return new UpdateData(this); 193 } 194 clear()195 public void clear() { 196 dataToUpdate.clear(); 197 dataToDelete.clear(); 198 nonIndexableKeys.clear(); 199 forceUpdate = false; 200 } 201 } 202 203 private final AtomicBoolean mIsAvailable = new AtomicBoolean(false); 204 private final UpdateData mDataToProcess = new UpdateData(); 205 private Context mContext; 206 private final String mBaseAuthority; 207 208 /** 209 * A basic singleton 210 */ getInstance(Context context)211 public static Index getInstance(Context context) { 212 if (sInstance == null) { 213 sInstance = new Index(context, BASE_AUTHORITY); 214 } else { 215 sInstance.setContext(context); 216 } 217 return sInstance; 218 } 219 Index(Context context, String baseAuthority)220 public Index(Context context, String baseAuthority) { 221 mContext = context; 222 mBaseAuthority = baseAuthority; 223 } 224 setContext(Context context)225 public void setContext(Context context) { 226 mContext = context; 227 } 228 isAvailable()229 public boolean isAvailable() { 230 return mIsAvailable.get(); 231 } 232 search(String query)233 public Cursor search(String query) { 234 final SQLiteDatabase database = getReadableDatabase(); 235 final Cursor[] cursors = new Cursor[2]; 236 237 final String primarySql = buildSearchSQL(query, MATCH_COLUMNS_PRIMARY, true); 238 Log.d(LOG_TAG, "Search primary query: " + primarySql); 239 cursors[0] = database.rawQuery(primarySql, null); 240 241 // We need to use an EXCEPT operator as negate MATCH queries do not work. 242 StringBuilder sql = new StringBuilder( 243 buildSearchSQL(query, MATCH_COLUMNS_SECONDARY, false)); 244 sql.append(" EXCEPT "); 245 sql.append(primarySql); 246 247 final String secondarySql = sql.toString(); 248 Log.d(LOG_TAG, "Search secondary query: " + secondarySql); 249 cursors[1] = database.rawQuery(secondarySql, null); 250 251 return new MergeCursor(cursors); 252 } 253 getSuggestions(String query)254 public Cursor getSuggestions(String query) { 255 final String sql = buildSuggestionsSQL(query); 256 Log.d(LOG_TAG, "Suggestions query: " + sql); 257 return getReadableDatabase().rawQuery(sql, null); 258 } 259 buildSuggestionsSQL(String query)260 private String buildSuggestionsSQL(String query) { 261 StringBuilder sb = new StringBuilder(); 262 263 sb.append("SELECT "); 264 sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY); 265 sb.append(" FROM "); 266 sb.append(Tables.TABLE_SAVED_QUERIES); 267 268 if (TextUtils.isEmpty(query)) { 269 sb.append(" ORDER BY rowId DESC"); 270 } else { 271 sb.append(" WHERE "); 272 sb.append(IndexDatabaseHelper.SavedQueriesColums.QUERY); 273 sb.append(" LIKE "); 274 sb.append("'"); 275 sb.append(query); 276 sb.append("%"); 277 sb.append("'"); 278 } 279 280 sb.append(" LIMIT "); 281 sb.append(MAX_PROPOSED_SUGGESTIONS); 282 283 return sb.toString(); 284 } 285 addSavedQuery(String query)286 public long addSavedQuery(String query){ 287 final SaveSearchQueryTask task = new SaveSearchQueryTask(); 288 task.execute(query); 289 try { 290 return task.get(); 291 } catch (InterruptedException e) { 292 Log.e(LOG_TAG, "Cannot insert saved query: " + query, e); 293 return -1 ; 294 } catch (ExecutionException e) { 295 Log.e(LOG_TAG, "Cannot insert saved query: " + query, e); 296 return -1; 297 } 298 } 299 update()300 public void update() { 301 final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); 302 List<ResolveInfo> list = 303 mContext.getPackageManager().queryIntentContentProviders(intent, 0); 304 305 final int size = list.size(); 306 for (int n = 0; n < size; n++) { 307 final ResolveInfo info = list.get(n); 308 if (!isWellKnownProvider(info)) { 309 continue; 310 } 311 final String authority = info.providerInfo.authority; 312 final String packageName = info.providerInfo.packageName; 313 314 addIndexablesFromRemoteProvider(packageName, authority); 315 addNonIndexablesKeysFromRemoteProvider(packageName, authority); 316 } 317 318 updateInternal(); 319 } 320 addIndexablesFromRemoteProvider(String packageName, String authority)321 private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { 322 try { 323 final int baseRank = Ranking.getBaseRankForAuthority(authority); 324 325 final Context context = mBaseAuthority.equals(authority) ? 326 mContext : mContext.createPackageContext(packageName, 0); 327 328 final Uri uriForResources = buildUriForXmlResources(authority); 329 addIndexablesForXmlResourceUri(context, packageName, uriForResources, 330 SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS, baseRank); 331 332 final Uri uriForRawData = buildUriForRawData(authority); 333 addIndexablesForRawDataUri(context, packageName, uriForRawData, 334 SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank); 335 return true; 336 } catch (PackageManager.NameNotFoundException e) { 337 Log.w(LOG_TAG, "Could not create context for " + packageName + ": " 338 + Log.getStackTraceString(e)); 339 return false; 340 } 341 } 342 addNonIndexablesKeysFromRemoteProvider(String packageName, String authority)343 private void addNonIndexablesKeysFromRemoteProvider(String packageName, 344 String authority) { 345 final List<String> keys = 346 getNonIndexablesKeysFromRemoteProvider(packageName, authority); 347 addNonIndexableKeys(packageName, keys); 348 } 349 getNonIndexablesKeysFromRemoteProvider(String packageName, String authority)350 private List<String> getNonIndexablesKeysFromRemoteProvider(String packageName, 351 String authority) { 352 try { 353 final Context packageContext = mContext.createPackageContext(packageName, 0); 354 355 final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); 356 return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, 357 SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); 358 } catch (PackageManager.NameNotFoundException e) { 359 Log.w(LOG_TAG, "Could not create context for " + packageName + ": " 360 + Log.getStackTraceString(e)); 361 return EMPTY_LIST; 362 } 363 } 364 getNonIndexablesKeys(Context packageContext, Uri uri, String[] projection)365 private List<String> getNonIndexablesKeys(Context packageContext, Uri uri, 366 String[] projection) { 367 368 final ContentResolver resolver = packageContext.getContentResolver(); 369 final Cursor cursor = resolver.query(uri, projection, null, null, null); 370 371 if (cursor == null) { 372 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); 373 return EMPTY_LIST; 374 } 375 376 List<String> result = new ArrayList<String>(); 377 try { 378 final int count = cursor.getCount(); 379 if (count > 0) { 380 while (cursor.moveToNext()) { 381 final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); 382 result.add(key); 383 } 384 } 385 return result; 386 } finally { 387 cursor.close(); 388 } 389 } 390 addIndexableData(SearchIndexableData data)391 public void addIndexableData(SearchIndexableData data) { 392 synchronized (mDataToProcess) { 393 mDataToProcess.dataToUpdate.add(data); 394 } 395 } 396 addIndexableData(SearchIndexableResource[] array)397 public void addIndexableData(SearchIndexableResource[] array) { 398 synchronized (mDataToProcess) { 399 final int count = array.length; 400 for (int n = 0; n < count; n++) { 401 mDataToProcess.dataToUpdate.add(array[n]); 402 } 403 } 404 } 405 deleteIndexableData(SearchIndexableData data)406 public void deleteIndexableData(SearchIndexableData data) { 407 synchronized (mDataToProcess) { 408 mDataToProcess.dataToDelete.add(data); 409 } 410 } 411 addNonIndexableKeys(String authority, List<String> keys)412 public void addNonIndexableKeys(String authority, List<String> keys) { 413 synchronized (mDataToProcess) { 414 mDataToProcess.nonIndexableKeys.put(authority, keys); 415 } 416 } 417 418 /** 419 * Only allow a "well known" SearchIndexablesProvider. The provider should: 420 * 421 * - have read/write {@link android.Manifest.permission#READ_SEARCH_INDEXABLES} 422 * - be from a privileged package 423 */ isWellKnownProvider(ResolveInfo info)424 private boolean isWellKnownProvider(ResolveInfo info) { 425 final String authority = info.providerInfo.authority; 426 final String packageName = info.providerInfo.applicationInfo.packageName; 427 428 if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { 429 return false; 430 } 431 432 final String readPermission = info.providerInfo.readPermission; 433 final String writePermission = info.providerInfo.writePermission; 434 435 if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { 436 return false; 437 } 438 439 if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || 440 !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { 441 return false; 442 } 443 444 return isPrivilegedPackage(packageName); 445 } 446 isPrivilegedPackage(String packageName)447 private boolean isPrivilegedPackage(String packageName) { 448 final PackageManager pm = mContext.getPackageManager(); 449 try { 450 PackageInfo packInfo = pm.getPackageInfo(packageName, 0); 451 return ((packInfo.applicationInfo.flags & ApplicationInfo.FLAG_PRIVILEGED) != 0); 452 } catch (PackageManager.NameNotFoundException e) { 453 return false; 454 } 455 } 456 updateFromRemoteProvider(String packageName, String authority)457 private void updateFromRemoteProvider(String packageName, String authority) { 458 if (addIndexablesFromRemoteProvider(packageName, authority)) { 459 updateInternal(); 460 } 461 } 462 463 /** 464 * Update the Index for a specific class name resources 465 * 466 * @param className the class name (typically a fragment name). 467 * @param rebuild true means that you want to delete the data from the Index first. 468 * @param includeInSearchResults true means that you want the bit "enabled" set so that the 469 * data will be seen included into the search results 470 */ updateFromClassNameResource(String className, boolean rebuild, boolean includeInSearchResults)471 public void updateFromClassNameResource(String className, boolean rebuild, 472 boolean includeInSearchResults) { 473 if (className == null) { 474 throw new IllegalArgumentException("class name cannot be null!"); 475 } 476 final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className); 477 if (res == null ) { 478 Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className); 479 return; 480 } 481 res.context = mContext; 482 res.enabled = includeInSearchResults; 483 if (rebuild) { 484 deleteIndexableData(res); 485 } 486 addIndexableData(res); 487 mDataToProcess.forceUpdate = true; 488 updateInternal(); 489 res.enabled = false; 490 } 491 updateFromSearchIndexableData(SearchIndexableData data)492 public void updateFromSearchIndexableData(SearchIndexableData data) { 493 addIndexableData(data); 494 mDataToProcess.forceUpdate = true; 495 updateInternal(); 496 } 497 getReadableDatabase()498 private SQLiteDatabase getReadableDatabase() { 499 return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase(); 500 } 501 getWritableDatabase()502 private SQLiteDatabase getWritableDatabase() { 503 return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); 504 } 505 buildUriForXmlResources(String authority)506 private static Uri buildUriForXmlResources(String authority) { 507 return Uri.parse("content://" + authority + "/" + 508 SearchIndexablesContract.INDEXABLES_XML_RES_PATH); 509 } 510 buildUriForRawData(String authority)511 private static Uri buildUriForRawData(String authority) { 512 return Uri.parse("content://" + authority + "/" + 513 SearchIndexablesContract.INDEXABLES_RAW_PATH); 514 } 515 buildUriForNonIndexableKeys(String authority)516 private static Uri buildUriForNonIndexableKeys(String authority) { 517 return Uri.parse("content://" + authority + "/" + 518 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); 519 } 520 updateInternal()521 private void updateInternal() { 522 synchronized (mDataToProcess) { 523 final UpdateIndexTask task = new UpdateIndexTask(); 524 UpdateData copy = mDataToProcess.copy(); 525 task.execute(copy); 526 mDataToProcess.clear(); 527 } 528 } 529 addIndexablesForXmlResourceUri(Context packageContext, String packageName, Uri uri, String[] projection, int baseRank)530 private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, 531 Uri uri, String[] projection, int baseRank) { 532 533 final ContentResolver resolver = packageContext.getContentResolver(); 534 final Cursor cursor = resolver.query(uri, projection, null, null, null); 535 536 if (cursor == null) { 537 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); 538 return; 539 } 540 541 try { 542 final int count = cursor.getCount(); 543 if (count > 0) { 544 while (cursor.moveToNext()) { 545 final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK); 546 final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; 547 548 final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); 549 550 final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); 551 final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); 552 553 final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); 554 final String targetPackage = cursor.getString( 555 COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); 556 final String targetClass = cursor.getString( 557 COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); 558 559 SearchIndexableResource sir = new SearchIndexableResource(packageContext); 560 sir.rank = rank; 561 sir.xmlResId = xmlResId; 562 sir.className = className; 563 sir.packageName = packageName; 564 sir.iconResId = iconResId; 565 sir.intentAction = action; 566 sir.intentTargetPackage = targetPackage; 567 sir.intentTargetClass = targetClass; 568 569 addIndexableData(sir); 570 } 571 } 572 } finally { 573 cursor.close(); 574 } 575 } 576 addIndexablesForRawDataUri(Context packageContext, String packageName, Uri uri, String[] projection, int baseRank)577 private void addIndexablesForRawDataUri(Context packageContext, String packageName, 578 Uri uri, String[] projection, int baseRank) { 579 580 final ContentResolver resolver = packageContext.getContentResolver(); 581 final Cursor cursor = resolver.query(uri, projection, null, null, null); 582 583 if (cursor == null) { 584 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); 585 return; 586 } 587 588 try { 589 final int count = cursor.getCount(); 590 if (count > 0) { 591 while (cursor.moveToNext()) { 592 final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); 593 final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; 594 595 final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); 596 final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); 597 final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); 598 final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); 599 final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); 600 601 final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); 602 603 final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); 604 final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); 605 606 final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); 607 final String targetPackage = cursor.getString( 608 COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); 609 final String targetClass = cursor.getString( 610 COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); 611 612 final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); 613 final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); 614 615 SearchIndexableRaw data = new SearchIndexableRaw(packageContext); 616 data.rank = rank; 617 data.title = title; 618 data.summaryOn = summaryOn; 619 data.summaryOff = summaryOff; 620 data.entries = entries; 621 data.keywords = keywords; 622 data.screenTitle = screenTitle; 623 data.className = className; 624 data.packageName = packageName; 625 data.iconResId = iconResId; 626 data.intentAction = action; 627 data.intentTargetPackage = targetPackage; 628 data.intentTargetClass = targetClass; 629 data.key = key; 630 data.userId = userId; 631 632 addIndexableData(data); 633 } 634 } 635 } finally { 636 cursor.close(); 637 } 638 } 639 buildSearchSQL(String query, String[] colums, boolean withOrderBy)640 private String buildSearchSQL(String query, String[] colums, boolean withOrderBy) { 641 StringBuilder sb = new StringBuilder(); 642 sb.append(buildSearchSQLForColumn(query, colums)); 643 if (withOrderBy) { 644 sb.append(" ORDER BY "); 645 sb.append(IndexColumns.DATA_RANK); 646 } 647 return sb.toString(); 648 } 649 buildSearchSQLForColumn(String query, String[] columnNames)650 private String buildSearchSQLForColumn(String query, String[] columnNames) { 651 StringBuilder sb = new StringBuilder(); 652 sb.append("SELECT "); 653 for (int n = 0; n < SELECT_COLUMNS.length; n++) { 654 sb.append(SELECT_COLUMNS[n]); 655 if (n < SELECT_COLUMNS.length - 1) { 656 sb.append(", "); 657 } 658 } 659 sb.append(" FROM "); 660 sb.append(Tables.TABLE_PREFS_INDEX); 661 sb.append(" WHERE "); 662 sb.append(buildSearchWhereStringForColumns(query, columnNames)); 663 664 return sb.toString(); 665 } 666 buildSearchWhereStringForColumns(String query, String[] columnNames)667 private String buildSearchWhereStringForColumns(String query, String[] columnNames) { 668 final StringBuilder sb = new StringBuilder(Tables.TABLE_PREFS_INDEX); 669 sb.append(" MATCH "); 670 DatabaseUtils.appendEscapedSQLString(sb, 671 buildSearchMatchStringForColumns(query, columnNames)); 672 sb.append(" AND "); 673 sb.append(IndexColumns.LOCALE); 674 sb.append(" = "); 675 DatabaseUtils.appendEscapedSQLString(sb, Locale.getDefault().toString()); 676 sb.append(" AND "); 677 sb.append(IndexColumns.ENABLED); 678 sb.append(" = 1"); 679 return sb.toString(); 680 } 681 buildSearchMatchStringForColumns(String query, String[] columnNames)682 private String buildSearchMatchStringForColumns(String query, String[] columnNames) { 683 final String value = query + "*"; 684 StringBuilder sb = new StringBuilder(); 685 final int count = columnNames.length; 686 for (int n = 0; n < count; n++) { 687 sb.append(columnNames[n]); 688 sb.append(":"); 689 sb.append(value); 690 if (n < count - 1) { 691 sb.append(" OR "); 692 } 693 } 694 return sb.toString(); 695 } 696 indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, SearchIndexableData data, Map<String, List<String>> nonIndexableKeys)697 private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, 698 SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) { 699 if (data instanceof SearchIndexableResource) { 700 indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); 701 } else if (data instanceof SearchIndexableRaw) { 702 indexOneRaw(database, localeStr, (SearchIndexableRaw) data); 703 } 704 } 705 indexOneRaw(SQLiteDatabase database, String localeStr, SearchIndexableRaw raw)706 private void indexOneRaw(SQLiteDatabase database, String localeStr, 707 SearchIndexableRaw raw) { 708 // Should be the same locale as the one we are processing 709 if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { 710 return; 711 } 712 713 updateOneRowWithFilteredData(database, localeStr, 714 raw.title, 715 raw.summaryOn, 716 raw.summaryOff, 717 raw.entries, 718 raw.className, 719 raw.screenTitle, 720 raw.iconResId, 721 raw.rank, 722 raw.keywords, 723 raw.intentAction, 724 raw.intentTargetPackage, 725 raw.intentTargetClass, 726 raw.enabled, 727 raw.key, 728 raw.userId); 729 } 730 isIndexableClass(final Class<?> clazz)731 private static boolean isIndexableClass(final Class<?> clazz) { 732 return (clazz != null) && Indexable.class.isAssignableFrom(clazz); 733 } 734 getIndexableClass(String className)735 private static Class<?> getIndexableClass(String className) { 736 final Class<?> clazz; 737 try { 738 clazz = Class.forName(className); 739 } catch (ClassNotFoundException e) { 740 Log.d(LOG_TAG, "Cannot find class: " + className); 741 return null; 742 } 743 return isIndexableClass(clazz) ? clazz : null; 744 } 745 indexOneResource(SQLiteDatabase database, String localeStr, SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource)746 private void indexOneResource(SQLiteDatabase database, String localeStr, 747 SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) { 748 749 if (sir == null) { 750 Log.e(LOG_TAG, "Cannot index a null resource!"); 751 return; 752 } 753 754 final List<String> nonIndexableKeys = new ArrayList<String>(); 755 756 if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) { 757 List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName); 758 if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) { 759 nonIndexableKeys.addAll(resNonIndxableKeys); 760 } 761 762 indexFromResource(sir.context, database, localeStr, 763 sir.xmlResId, sir.className, sir.iconResId, sir.rank, 764 sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass, 765 nonIndexableKeys); 766 } else { 767 if (TextUtils.isEmpty(sir.className)) { 768 Log.w(LOG_TAG, "Cannot index an empty Search Provider name!"); 769 return; 770 } 771 772 final Class<?> clazz = getIndexableClass(sir.className); 773 if (clazz == null) { 774 Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className + 775 "' should implement the " + Indexable.class.getName() + " interface!"); 776 return; 777 } 778 779 // Will be non null only for a Local provider implementing a 780 // SEARCH_INDEX_DATA_PROVIDER field 781 final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz); 782 if (provider != null) { 783 List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context); 784 if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) { 785 nonIndexableKeys.addAll(providerNonIndexableKeys); 786 } 787 788 indexFromProvider(mContext, database, localeStr, provider, sir.className, 789 sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys); 790 } 791 } 792 } 793 getSearchIndexProvider(final Class<?> clazz)794 private Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) { 795 try { 796 final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); 797 return (Indexable.SearchIndexProvider) f.get(null); 798 } catch (NoSuchFieldException e) { 799 Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 800 } catch (SecurityException se) { 801 Log.d(LOG_TAG, 802 "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 803 } catch (IllegalAccessException e) { 804 Log.d(LOG_TAG, 805 "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 806 } catch (IllegalArgumentException e) { 807 Log.d(LOG_TAG, 808 "Illegal argument when accessing field '" + 809 FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 810 } 811 return null; 812 } 813 indexFromResource(Context context, SQLiteDatabase database, String localeStr, int xmlResId, String fragmentName, int iconResId, int rank, String intentAction, String intentTargetPackage, String intentTargetClass, List<String> nonIndexableKeys)814 private void indexFromResource(Context context, SQLiteDatabase database, String localeStr, 815 int xmlResId, String fragmentName, int iconResId, int rank, 816 String intentAction, String intentTargetPackage, String intentTargetClass, 817 List<String> nonIndexableKeys) { 818 819 XmlResourceParser parser = null; 820 try { 821 parser = context.getResources().getXml(xmlResId); 822 823 int type; 824 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 825 && type != XmlPullParser.START_TAG) { 826 // Parse next until start tag is found 827 } 828 829 String nodeName = parser.getName(); 830 if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { 831 throw new RuntimeException( 832 "XML document must start with <PreferenceScreen> tag; found" 833 + nodeName + " at " + parser.getPositionDescription()); 834 } 835 836 final int outerDepth = parser.getDepth(); 837 final AttributeSet attrs = Xml.asAttributeSet(parser); 838 839 final String screenTitle = getDataTitle(context, attrs); 840 841 String key = getDataKey(context, attrs); 842 843 String title; 844 String summary; 845 String keywords; 846 847 // Insert rows for the main PreferenceScreen node. Rewrite the data for removing 848 // hyphens. 849 if (!nonIndexableKeys.contains(key)) { 850 title = getDataTitle(context, attrs); 851 summary = getDataSummary(context, attrs); 852 keywords = getDataKeywords(context, attrs); 853 854 updateOneRowWithFilteredData(database, localeStr, title, summary, null, null, 855 fragmentName, screenTitle, iconResId, rank, 856 keywords, intentAction, intentTargetPackage, intentTargetClass, true, 857 key, -1 /* default user id */); 858 } 859 860 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 861 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 862 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 863 continue; 864 } 865 866 nodeName = parser.getName(); 867 868 key = getDataKey(context, attrs); 869 if (nonIndexableKeys.contains(key)) { 870 continue; 871 } 872 873 title = getDataTitle(context, attrs); 874 keywords = getDataKeywords(context, attrs); 875 876 if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { 877 summary = getDataSummary(context, attrs); 878 879 String entries = null; 880 881 if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { 882 entries = getDataEntries(context, attrs); 883 } 884 885 // Insert rows for the child nodes of PreferenceScreen 886 updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries, 887 fragmentName, screenTitle, iconResId, rank, 888 keywords, intentAction, intentTargetPackage, intentTargetClass, 889 true, key, -1 /* default user id */); 890 } else { 891 String summaryOn = getDataSummaryOn(context, attrs); 892 String summaryOff = getDataSummaryOff(context, attrs); 893 894 if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { 895 summaryOn = getDataSummary(context, attrs); 896 } 897 898 updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff, 899 null, fragmentName, screenTitle, iconResId, rank, 900 keywords, intentAction, intentTargetPackage, intentTargetClass, 901 true, key, -1 /* default user id */); 902 } 903 } 904 905 } catch (XmlPullParserException e) { 906 throw new RuntimeException("Error parsing PreferenceScreen", e); 907 } catch (IOException e) { 908 throw new RuntimeException("Error parsing PreferenceScreen", e); 909 } finally { 910 if (parser != null) parser.close(); 911 } 912 } 913 indexFromProvider(Context context, SQLiteDatabase database, String localeStr, Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, boolean enabled, List<String> nonIndexableKeys)914 private void indexFromProvider(Context context, SQLiteDatabase database, String localeStr, 915 Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, 916 boolean enabled, List<String> nonIndexableKeys) { 917 918 if (provider == null) { 919 Log.w(LOG_TAG, "Cannot find provider: " + className); 920 return; 921 } 922 923 final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(context, enabled); 924 925 if (rawList != null) { 926 final int rawSize = rawList.size(); 927 for (int i = 0; i < rawSize; i++) { 928 SearchIndexableRaw raw = rawList.get(i); 929 930 // Should be the same locale as the one we are processing 931 if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { 932 continue; 933 } 934 935 if (nonIndexableKeys.contains(raw.key)) { 936 continue; 937 } 938 939 updateOneRowWithFilteredData(database, localeStr, 940 raw.title, 941 raw.summaryOn, 942 raw.summaryOff, 943 raw.entries, 944 className, 945 raw.screenTitle, 946 iconResId, 947 rank, 948 raw.keywords, 949 raw.intentAction, 950 raw.intentTargetPackage, 951 raw.intentTargetClass, 952 raw.enabled, 953 raw.key, 954 raw.userId); 955 } 956 } 957 958 final List<SearchIndexableResource> resList = 959 provider.getXmlResourcesToIndex(context, enabled); 960 if (resList != null) { 961 final int resSize = resList.size(); 962 for (int i = 0; i < resSize; i++) { 963 SearchIndexableResource item = resList.get(i); 964 965 // Should be the same locale as the one we are processing 966 if (!item.locale.toString().equalsIgnoreCase(localeStr)) { 967 continue; 968 } 969 970 final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId; 971 final int itemRank = (item.rank == 0) ? rank : item.rank; 972 String itemClassName = (TextUtils.isEmpty(item.className)) 973 ? className : item.className; 974 975 indexFromResource(context, database, localeStr, 976 item.xmlResId, itemClassName, itemIconResId, itemRank, 977 item.intentAction, item.intentTargetPackage, 978 item.intentTargetClass, nonIndexableKeys); 979 } 980 } 981 } 982 updateOneRowWithFilteredData(SQLiteDatabase database, String locale, String title, String summaryOn, String summaryOff, String entries, String className, String screenTitle, int iconResId, int rank, String keywords, String intentAction, String intentTargetPackage, String intentTargetClass, boolean enabled, String key, int userId)983 private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale, 984 String title, String summaryOn, String summaryOff, String entries, 985 String className, 986 String screenTitle, int iconResId, int rank, String keywords, 987 String intentAction, String intentTargetPackage, String intentTargetClass, 988 boolean enabled, String key, int userId) { 989 990 final String updatedTitle = normalizeHyphen(title); 991 final String updatedSummaryOn = normalizeHyphen(summaryOn); 992 final String updatedSummaryOff = normalizeHyphen(summaryOff); 993 994 final String normalizedTitle = normalizeString(updatedTitle); 995 final String normalizedSummaryOn = normalizeString(updatedSummaryOn); 996 final String normalizedSummaryOff = normalizeString(updatedSummaryOff); 997 998 updateOneRow(database, locale, 999 updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn, 1000 updatedSummaryOff, normalizedSummaryOff, entries, 1001 className, screenTitle, iconResId, 1002 rank, keywords, intentAction, intentTargetPackage, intentTargetClass, enabled, 1003 key, userId); 1004 } 1005 normalizeHyphen(String input)1006 private static String normalizeHyphen(String input) { 1007 return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; 1008 } 1009 normalizeString(String input)1010 private static String normalizeString(String input) { 1011 final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; 1012 final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); 1013 1014 return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); 1015 } 1016 updateOneRow(SQLiteDatabase database, String locale, String updatedTitle, String normalizedTitle, String updatedSummaryOn, String normalizedSummaryOn, String updatedSummaryOff, String normalizedSummaryOff, String entries, String className, String screenTitle, int iconResId, int rank, String keywords, String intentAction, String intentTargetPackage, String intentTargetClass, boolean enabled, String key, int userId)1017 private void updateOneRow(SQLiteDatabase database, String locale, 1018 String updatedTitle, String normalizedTitle, 1019 String updatedSummaryOn, String normalizedSummaryOn, 1020 String updatedSummaryOff, String normalizedSummaryOff, String entries, 1021 String className, String screenTitle, int iconResId, int rank, String keywords, 1022 String intentAction, String intentTargetPackage, String intentTargetClass, 1023 boolean enabled, String key, int userId) { 1024 1025 if (TextUtils.isEmpty(updatedTitle)) { 1026 return; 1027 } 1028 1029 // The DocID should contains more than the title string itself (you may have two settings 1030 // with the same title). So we need to use a combination of the title and the screenTitle. 1031 StringBuilder sb = new StringBuilder(updatedTitle); 1032 sb.append(screenTitle); 1033 int docId = sb.toString().hashCode(); 1034 1035 ContentValues values = new ContentValues(); 1036 values.put(IndexColumns.DOCID, docId); 1037 values.put(IndexColumns.LOCALE, locale); 1038 values.put(IndexColumns.DATA_RANK, rank); 1039 values.put(IndexColumns.DATA_TITLE, updatedTitle); 1040 values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle); 1041 values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn); 1042 values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn); 1043 values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff); 1044 values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff); 1045 values.put(IndexColumns.DATA_ENTRIES, entries); 1046 values.put(IndexColumns.DATA_KEYWORDS, keywords); 1047 values.put(IndexColumns.CLASS_NAME, className); 1048 values.put(IndexColumns.SCREEN_TITLE, screenTitle); 1049 values.put(IndexColumns.INTENT_ACTION, intentAction); 1050 values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage); 1051 values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass); 1052 values.put(IndexColumns.ICON, iconResId); 1053 values.put(IndexColumns.ENABLED, enabled); 1054 values.put(IndexColumns.DATA_KEY_REF, key); 1055 values.put(IndexColumns.USER_ID, userId); 1056 1057 database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values); 1058 } 1059 getDataKey(Context context, AttributeSet attrs)1060 private String getDataKey(Context context, AttributeSet attrs) { 1061 return getData(context, attrs, 1062 com.android.internal.R.styleable.Preference, 1063 com.android.internal.R.styleable.Preference_key); 1064 } 1065 getDataTitle(Context context, AttributeSet attrs)1066 private String getDataTitle(Context context, AttributeSet attrs) { 1067 return getData(context, attrs, 1068 com.android.internal.R.styleable.Preference, 1069 com.android.internal.R.styleable.Preference_title); 1070 } 1071 getDataSummary(Context context, AttributeSet attrs)1072 private String getDataSummary(Context context, AttributeSet attrs) { 1073 return getData(context, attrs, 1074 com.android.internal.R.styleable.Preference, 1075 com.android.internal.R.styleable.Preference_summary); 1076 } 1077 getDataSummaryOn(Context context, AttributeSet attrs)1078 private String getDataSummaryOn(Context context, AttributeSet attrs) { 1079 return getData(context, attrs, 1080 com.android.internal.R.styleable.CheckBoxPreference, 1081 com.android.internal.R.styleable.CheckBoxPreference_summaryOn); 1082 } 1083 getDataSummaryOff(Context context, AttributeSet attrs)1084 private String getDataSummaryOff(Context context, AttributeSet attrs) { 1085 return getData(context, attrs, 1086 com.android.internal.R.styleable.CheckBoxPreference, 1087 com.android.internal.R.styleable.CheckBoxPreference_summaryOff); 1088 } 1089 getDataEntries(Context context, AttributeSet attrs)1090 private String getDataEntries(Context context, AttributeSet attrs) { 1091 return getDataEntries(context, attrs, 1092 com.android.internal.R.styleable.ListPreference, 1093 com.android.internal.R.styleable.ListPreference_entries); 1094 } 1095 getDataKeywords(Context context, AttributeSet attrs)1096 private String getDataKeywords(Context context, AttributeSet attrs) { 1097 return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); 1098 } 1099 getData(Context context, AttributeSet set, int[] attrs, int resId)1100 private String getData(Context context, AttributeSet set, int[] attrs, int resId) { 1101 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1102 final TypedValue tv = sa.peekValue(resId); 1103 1104 CharSequence data = null; 1105 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 1106 if (tv.resourceId != 0) { 1107 data = context.getText(tv.resourceId); 1108 } else { 1109 data = tv.string; 1110 } 1111 } 1112 return (data != null) ? data.toString() : null; 1113 } 1114 getDataEntries(Context context, AttributeSet set, int[] attrs, int resId)1115 private String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) { 1116 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1117 final TypedValue tv = sa.peekValue(resId); 1118 1119 String[] data = null; 1120 if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { 1121 if (tv.resourceId != 0) { 1122 data = context.getResources().getStringArray(tv.resourceId); 1123 } 1124 } 1125 final int count = (data == null ) ? 0 : data.length; 1126 if (count == 0) { 1127 return null; 1128 } 1129 final StringBuilder result = new StringBuilder(); 1130 for (int n = 0; n < count; n++) { 1131 result.append(data[n]); 1132 result.append(ENTRIES_SEPARATOR); 1133 } 1134 return result.toString(); 1135 } 1136 getResId(Context context, AttributeSet set, int[] attrs, int resId)1137 private int getResId(Context context, AttributeSet set, int[] attrs, int resId) { 1138 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1139 final TypedValue tv = sa.peekValue(resId); 1140 1141 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 1142 return tv.resourceId; 1143 } else { 1144 return 0; 1145 } 1146 } 1147 1148 /** 1149 * A private class for updating the Index database 1150 */ 1151 private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> { 1152 1153 @Override onPreExecute()1154 protected void onPreExecute() { 1155 super.onPreExecute(); 1156 mIsAvailable.set(false); 1157 } 1158 1159 @Override onPostExecute(Void aVoid)1160 protected void onPostExecute(Void aVoid) { 1161 super.onPostExecute(aVoid); 1162 mIsAvailable.set(true); 1163 } 1164 1165 @Override doInBackground(UpdateData... params)1166 protected Void doInBackground(UpdateData... params) { 1167 final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate; 1168 final List<SearchIndexableData> dataToDelete = params[0].dataToDelete; 1169 final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys; 1170 1171 final boolean forceUpdate = params[0].forceUpdate; 1172 1173 final SQLiteDatabase database = getWritableDatabase(); 1174 final String localeStr = Locale.getDefault().toString(); 1175 1176 try { 1177 database.beginTransaction(); 1178 if (dataToDelete.size() > 0) { 1179 processDataToDelete(database, localeStr, dataToDelete); 1180 } 1181 if (dataToUpdate.size() > 0) { 1182 processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, 1183 forceUpdate); 1184 } 1185 database.setTransactionSuccessful(); 1186 } finally { 1187 database.endTransaction(); 1188 } 1189 1190 return null; 1191 } 1192 processDataToUpdate(SQLiteDatabase database, String localeStr, List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, boolean forceUpdate)1193 private boolean processDataToUpdate(SQLiteDatabase database, String localeStr, 1194 List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, 1195 boolean forceUpdate) { 1196 1197 if (!forceUpdate && isLocaleAlreadyIndexed(database, localeStr)) { 1198 Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed"); 1199 return true; 1200 } 1201 1202 boolean result = false; 1203 final long current = System.currentTimeMillis(); 1204 1205 final int count = dataToUpdate.size(); 1206 for (int n = 0; n < count; n++) { 1207 final SearchIndexableData data = dataToUpdate.get(n); 1208 try { 1209 indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys); 1210 } catch (Exception e) { 1211 Log.e(LOG_TAG, 1212 "Cannot index: " + data.className + " for locale: " + localeStr, e); 1213 } 1214 } 1215 1216 final long now = System.currentTimeMillis(); 1217 Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " + 1218 (now - current) + " millis"); 1219 return result; 1220 } 1221 processDataToDelete(SQLiteDatabase database, String localeStr, List<SearchIndexableData> dataToDelete)1222 private boolean processDataToDelete(SQLiteDatabase database, String localeStr, 1223 List<SearchIndexableData> dataToDelete) { 1224 1225 boolean result = false; 1226 final long current = System.currentTimeMillis(); 1227 1228 final int count = dataToDelete.size(); 1229 for (int n = 0; n < count; n++) { 1230 final SearchIndexableData data = dataToDelete.get(n); 1231 if (data == null) { 1232 continue; 1233 } 1234 if (!TextUtils.isEmpty(data.className)) { 1235 delete(database, IndexColumns.CLASS_NAME, data.className); 1236 } else { 1237 if (data instanceof SearchIndexableRaw) { 1238 final SearchIndexableRaw raw = (SearchIndexableRaw) data; 1239 if (!TextUtils.isEmpty(raw.title)) { 1240 delete(database, IndexColumns.DATA_TITLE, raw.title); 1241 } 1242 } 1243 } 1244 } 1245 1246 final long now = System.currentTimeMillis(); 1247 Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " + 1248 (now - current) + " millis"); 1249 return result; 1250 } 1251 delete(SQLiteDatabase database, String columName, String value)1252 private int delete(SQLiteDatabase database, String columName, String value) { 1253 final String whereClause = columName + "=?"; 1254 final String[] whereArgs = new String[] { value }; 1255 1256 return database.delete(Tables.TABLE_PREFS_INDEX, whereClause, whereArgs); 1257 } 1258 isLocaleAlreadyIndexed(SQLiteDatabase database, String locale)1259 private boolean isLocaleAlreadyIndexed(SQLiteDatabase database, String locale) { 1260 Cursor cursor = null; 1261 boolean result = false; 1262 final StringBuilder sb = new StringBuilder(IndexColumns.LOCALE); 1263 sb.append(" = "); 1264 DatabaseUtils.appendEscapedSQLString(sb, locale); 1265 try { 1266 // We care only for 1 row 1267 cursor = database.query(Tables.TABLE_PREFS_INDEX, null, 1268 sb.toString(), null, null, null, null, "1"); 1269 final int count = cursor.getCount(); 1270 result = (count >= 1); 1271 } finally { 1272 if (cursor != null) { 1273 cursor.close(); 1274 } 1275 } 1276 return result; 1277 } 1278 } 1279 1280 /** 1281 * A basic AsyncTask for saving a Search query into the database 1282 */ 1283 private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> { 1284 1285 @Override doInBackground(String... params)1286 protected Long doInBackground(String... params) { 1287 final long now = new Date().getTime(); 1288 1289 final ContentValues values = new ContentValues(); 1290 values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]); 1291 values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now); 1292 1293 final SQLiteDatabase database = getWritableDatabase(); 1294 1295 long lastInsertedRowId = -1; 1296 try { 1297 // First, delete all saved queries that are the same 1298 database.delete(Tables.TABLE_SAVED_QUERIES, 1299 IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?", 1300 new String[] { params[0] }); 1301 1302 // Second, insert the saved query 1303 lastInsertedRowId = 1304 database.insertOrThrow(Tables.TABLE_SAVED_QUERIES, null, values); 1305 1306 // Last, remove "old" saved queries 1307 final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY; 1308 if (delta > 0) { 1309 int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?", 1310 new String[] { Long.toString(delta) }); 1311 Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)"); 1312 } 1313 } catch (Exception e) { 1314 Log.d(LOG_TAG, "Cannot update saved Search queries", e); 1315 } 1316 1317 return lastInsertedRowId; 1318 } 1319 } 1320 } 1321