Index.java revision cc254f4da96506901268c4a0b1d3cfacb5f44948
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 17package com.android.settings.search; 18 19import android.content.ContentResolver; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.Intent; 23import android.content.pm.ApplicationInfo; 24import android.content.pm.PackageInfo; 25import android.content.pm.PackageManager; 26import android.content.pm.ResolveInfo; 27import android.content.res.TypedArray; 28import android.content.res.XmlResourceParser; 29import android.database.Cursor; 30import android.database.DatabaseUtils; 31import android.database.MergeCursor; 32import android.database.sqlite.SQLiteDatabase; 33import android.net.Uri; 34import android.os.AsyncTask; 35import android.provider.SearchIndexableData; 36import android.provider.SearchIndexableResource; 37import android.provider.SearchIndexablesContract; 38import android.text.TextUtils; 39import android.util.AttributeSet; 40import android.util.Log; 41import android.util.TypedValue; 42import android.util.Xml; 43import com.android.settings.R; 44import org.xmlpull.v1.XmlPullParser; 45import org.xmlpull.v1.XmlPullParserException; 46 47import java.io.IOException; 48import java.lang.reflect.Field; 49import java.text.Normalizer; 50import java.util.ArrayList; 51import java.util.Collections; 52import java.util.Date; 53import java.util.HashMap; 54import java.util.List; 55import java.util.Locale; 56import java.util.Map; 57import java.util.concurrent.ExecutionException; 58import java.util.concurrent.atomic.AtomicBoolean; 59import java.util.regex.Pattern; 60 61import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; 62import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; 63import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; 64import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; 65import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; 66import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; 67import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; 68import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; 69import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; 70import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; 71import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; 72import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; 73import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; 74import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; 75import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; 76 77import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK; 78import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; 79import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; 80import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; 81import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; 82import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; 83import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; 84 85import static com.android.settings.search.IndexDatabaseHelper.Tables; 86import static com.android.settings.search.IndexDatabaseHelper.IndexColumns; 87 88public 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 178 public UpdateData() { 179 dataToUpdate = new ArrayList<SearchIndexableData>(); 180 dataToDelete = new ArrayList<SearchIndexableData>(); 181 nonIndexableKeys = new HashMap<String, List<String>>(); 182 } 183 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 191 public UpdateData copy() { 192 return new UpdateData(this); 193 } 194 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 */ 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 220 public Index(Context context, String baseAuthority) { 221 mContext = context; 222 mBaseAuthority = baseAuthority; 223 } 224 225 public void setContext(Context context) { 226 mContext = context; 227 } 228 229 public boolean isAvailable() { 230 return mIsAvailable.get(); 231 } 232 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 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 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 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 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 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 343 private void addNonIndexablesKeysFromRemoteProvider(String packageName, 344 String authority) { 345 final List<String> keys = 346 getNonIndexablesKeysFromRemoteProvider(packageName, authority); 347 addNonIndexableKeys(packageName, keys); 348 } 349 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 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 391 public void addIndexableData(SearchIndexableData data) { 392 synchronized (mDataToProcess) { 393 mDataToProcess.dataToUpdate.add(data); 394 } 395 } 396 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 406 public void deleteIndexableData(SearchIndexableData data) { 407 synchronized (mDataToProcess) { 408 mDataToProcess.dataToDelete.add(data); 409 } 410 } 411 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 */ 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 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.privateFlags 452 & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0); 453 } catch (PackageManager.NameNotFoundException e) { 454 return false; 455 } 456 } 457 458 private void updateFromRemoteProvider(String packageName, String authority) { 459 if (addIndexablesFromRemoteProvider(packageName, authority)) { 460 updateInternal(); 461 } 462 } 463 464 /** 465 * Update the Index for a specific class name resources 466 * 467 * @param className the class name (typically a fragment name). 468 * @param rebuild true means that you want to delete the data from the Index first. 469 * @param includeInSearchResults true means that you want the bit "enabled" set so that the 470 * data will be seen included into the search results 471 */ 472 public void updateFromClassNameResource(String className, boolean rebuild, 473 boolean includeInSearchResults) { 474 if (className == null) { 475 throw new IllegalArgumentException("class name cannot be null!"); 476 } 477 final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className); 478 if (res == null ) { 479 Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className); 480 return; 481 } 482 res.context = mContext; 483 res.enabled = includeInSearchResults; 484 if (rebuild) { 485 deleteIndexableData(res); 486 } 487 addIndexableData(res); 488 mDataToProcess.forceUpdate = true; 489 updateInternal(); 490 res.enabled = false; 491 } 492 493 public void updateFromSearchIndexableData(SearchIndexableData data) { 494 addIndexableData(data); 495 mDataToProcess.forceUpdate = true; 496 updateInternal(); 497 } 498 499 private SQLiteDatabase getReadableDatabase() { 500 return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase(); 501 } 502 503 private SQLiteDatabase getWritableDatabase() { 504 return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); 505 } 506 507 private static Uri buildUriForXmlResources(String authority) { 508 return Uri.parse("content://" + authority + "/" + 509 SearchIndexablesContract.INDEXABLES_XML_RES_PATH); 510 } 511 512 private static Uri buildUriForRawData(String authority) { 513 return Uri.parse("content://" + authority + "/" + 514 SearchIndexablesContract.INDEXABLES_RAW_PATH); 515 } 516 517 private static Uri buildUriForNonIndexableKeys(String authority) { 518 return Uri.parse("content://" + authority + "/" + 519 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); 520 } 521 522 private void updateInternal() { 523 synchronized (mDataToProcess) { 524 final UpdateIndexTask task = new UpdateIndexTask(); 525 UpdateData copy = mDataToProcess.copy(); 526 task.execute(copy); 527 mDataToProcess.clear(); 528 } 529 } 530 531 private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, 532 Uri uri, String[] projection, int baseRank) { 533 534 final ContentResolver resolver = packageContext.getContentResolver(); 535 final Cursor cursor = resolver.query(uri, projection, null, null, null); 536 537 if (cursor == null) { 538 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); 539 return; 540 } 541 542 try { 543 final int count = cursor.getCount(); 544 if (count > 0) { 545 while (cursor.moveToNext()) { 546 final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK); 547 final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; 548 549 final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); 550 551 final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); 552 final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); 553 554 final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); 555 final String targetPackage = cursor.getString( 556 COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); 557 final String targetClass = cursor.getString( 558 COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); 559 560 SearchIndexableResource sir = new SearchIndexableResource(packageContext); 561 sir.rank = rank; 562 sir.xmlResId = xmlResId; 563 sir.className = className; 564 sir.packageName = packageName; 565 sir.iconResId = iconResId; 566 sir.intentAction = action; 567 sir.intentTargetPackage = targetPackage; 568 sir.intentTargetClass = targetClass; 569 570 addIndexableData(sir); 571 } 572 } 573 } finally { 574 cursor.close(); 575 } 576 } 577 578 private void addIndexablesForRawDataUri(Context packageContext, String packageName, 579 Uri uri, String[] projection, int baseRank) { 580 581 final ContentResolver resolver = packageContext.getContentResolver(); 582 final Cursor cursor = resolver.query(uri, projection, null, null, null); 583 584 if (cursor == null) { 585 Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); 586 return; 587 } 588 589 try { 590 final int count = cursor.getCount(); 591 if (count > 0) { 592 while (cursor.moveToNext()) { 593 final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); 594 final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; 595 596 final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); 597 final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); 598 final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); 599 final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); 600 final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); 601 602 final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); 603 604 final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); 605 final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); 606 607 final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); 608 final String targetPackage = cursor.getString( 609 COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); 610 final String targetClass = cursor.getString( 611 COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); 612 613 final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); 614 final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); 615 616 SearchIndexableRaw data = new SearchIndexableRaw(packageContext); 617 data.rank = rank; 618 data.title = title; 619 data.summaryOn = summaryOn; 620 data.summaryOff = summaryOff; 621 data.entries = entries; 622 data.keywords = keywords; 623 data.screenTitle = screenTitle; 624 data.className = className; 625 data.packageName = packageName; 626 data.iconResId = iconResId; 627 data.intentAction = action; 628 data.intentTargetPackage = targetPackage; 629 data.intentTargetClass = targetClass; 630 data.key = key; 631 data.userId = userId; 632 633 addIndexableData(data); 634 } 635 } 636 } finally { 637 cursor.close(); 638 } 639 } 640 641 private String buildSearchSQL(String query, String[] colums, boolean withOrderBy) { 642 StringBuilder sb = new StringBuilder(); 643 sb.append(buildSearchSQLForColumn(query, colums)); 644 if (withOrderBy) { 645 sb.append(" ORDER BY "); 646 sb.append(IndexColumns.DATA_RANK); 647 } 648 return sb.toString(); 649 } 650 651 private String buildSearchSQLForColumn(String query, String[] columnNames) { 652 StringBuilder sb = new StringBuilder(); 653 sb.append("SELECT "); 654 for (int n = 0; n < SELECT_COLUMNS.length; n++) { 655 sb.append(SELECT_COLUMNS[n]); 656 if (n < SELECT_COLUMNS.length - 1) { 657 sb.append(", "); 658 } 659 } 660 sb.append(" FROM "); 661 sb.append(Tables.TABLE_PREFS_INDEX); 662 sb.append(" WHERE "); 663 sb.append(buildSearchWhereStringForColumns(query, columnNames)); 664 665 return sb.toString(); 666 } 667 668 private String buildSearchWhereStringForColumns(String query, String[] columnNames) { 669 final StringBuilder sb = new StringBuilder(Tables.TABLE_PREFS_INDEX); 670 sb.append(" MATCH "); 671 DatabaseUtils.appendEscapedSQLString(sb, 672 buildSearchMatchStringForColumns(query, columnNames)); 673 sb.append(" AND "); 674 sb.append(IndexColumns.LOCALE); 675 sb.append(" = "); 676 DatabaseUtils.appendEscapedSQLString(sb, Locale.getDefault().toString()); 677 sb.append(" AND "); 678 sb.append(IndexColumns.ENABLED); 679 sb.append(" = 1"); 680 return sb.toString(); 681 } 682 683 private String buildSearchMatchStringForColumns(String query, String[] columnNames) { 684 final String value = query + "*"; 685 StringBuilder sb = new StringBuilder(); 686 final int count = columnNames.length; 687 for (int n = 0; n < count; n++) { 688 sb.append(columnNames[n]); 689 sb.append(":"); 690 sb.append(value); 691 if (n < count - 1) { 692 sb.append(" OR "); 693 } 694 } 695 return sb.toString(); 696 } 697 698 private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, 699 SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) { 700 if (data instanceof SearchIndexableResource) { 701 indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); 702 } else if (data instanceof SearchIndexableRaw) { 703 indexOneRaw(database, localeStr, (SearchIndexableRaw) data); 704 } 705 } 706 707 private void indexOneRaw(SQLiteDatabase database, String localeStr, 708 SearchIndexableRaw raw) { 709 // Should be the same locale as the one we are processing 710 if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { 711 return; 712 } 713 714 updateOneRowWithFilteredData(database, localeStr, 715 raw.title, 716 raw.summaryOn, 717 raw.summaryOff, 718 raw.entries, 719 raw.className, 720 raw.screenTitle, 721 raw.iconResId, 722 raw.rank, 723 raw.keywords, 724 raw.intentAction, 725 raw.intentTargetPackage, 726 raw.intentTargetClass, 727 raw.enabled, 728 raw.key, 729 raw.userId); 730 } 731 732 private static boolean isIndexableClass(final Class<?> clazz) { 733 return (clazz != null) && Indexable.class.isAssignableFrom(clazz); 734 } 735 736 private static Class<?> getIndexableClass(String className) { 737 final Class<?> clazz; 738 try { 739 clazz = Class.forName(className); 740 } catch (ClassNotFoundException e) { 741 Log.d(LOG_TAG, "Cannot find class: " + className); 742 return null; 743 } 744 return isIndexableClass(clazz) ? clazz : null; 745 } 746 747 private void indexOneResource(SQLiteDatabase database, String localeStr, 748 SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) { 749 750 if (sir == null) { 751 Log.e(LOG_TAG, "Cannot index a null resource!"); 752 return; 753 } 754 755 final List<String> nonIndexableKeys = new ArrayList<String>(); 756 757 if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) { 758 List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName); 759 if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) { 760 nonIndexableKeys.addAll(resNonIndxableKeys); 761 } 762 763 indexFromResource(sir.context, database, localeStr, 764 sir.xmlResId, sir.className, sir.iconResId, sir.rank, 765 sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass, 766 nonIndexableKeys); 767 } else { 768 if (TextUtils.isEmpty(sir.className)) { 769 Log.w(LOG_TAG, "Cannot index an empty Search Provider name!"); 770 return; 771 } 772 773 final Class<?> clazz = getIndexableClass(sir.className); 774 if (clazz == null) { 775 Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className + 776 "' should implement the " + Indexable.class.getName() + " interface!"); 777 return; 778 } 779 780 // Will be non null only for a Local provider implementing a 781 // SEARCH_INDEX_DATA_PROVIDER field 782 final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz); 783 if (provider != null) { 784 List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context); 785 if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) { 786 nonIndexableKeys.addAll(providerNonIndexableKeys); 787 } 788 789 indexFromProvider(mContext, database, localeStr, provider, sir.className, 790 sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys); 791 } 792 } 793 } 794 795 private Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) { 796 try { 797 final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); 798 return (Indexable.SearchIndexProvider) f.get(null); 799 } catch (NoSuchFieldException e) { 800 Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 801 } catch (SecurityException se) { 802 Log.d(LOG_TAG, 803 "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 804 } catch (IllegalAccessException e) { 805 Log.d(LOG_TAG, 806 "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 807 } catch (IllegalArgumentException e) { 808 Log.d(LOG_TAG, 809 "Illegal argument when accessing field '" + 810 FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); 811 } 812 return null; 813 } 814 815 private void indexFromResource(Context context, SQLiteDatabase database, String localeStr, 816 int xmlResId, String fragmentName, int iconResId, int rank, 817 String intentAction, String intentTargetPackage, String intentTargetClass, 818 List<String> nonIndexableKeys) { 819 820 XmlResourceParser parser = null; 821 try { 822 parser = context.getResources().getXml(xmlResId); 823 824 int type; 825 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 826 && type != XmlPullParser.START_TAG) { 827 // Parse next until start tag is found 828 } 829 830 String nodeName = parser.getName(); 831 if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { 832 throw new RuntimeException( 833 "XML document must start with <PreferenceScreen> tag; found" 834 + nodeName + " at " + parser.getPositionDescription()); 835 } 836 837 final int outerDepth = parser.getDepth(); 838 final AttributeSet attrs = Xml.asAttributeSet(parser); 839 840 final String screenTitle = getDataTitle(context, attrs); 841 842 String key = getDataKey(context, attrs); 843 844 String title; 845 String summary; 846 String keywords; 847 848 // Insert rows for the main PreferenceScreen node. Rewrite the data for removing 849 // hyphens. 850 if (!nonIndexableKeys.contains(key)) { 851 title = getDataTitle(context, attrs); 852 summary = getDataSummary(context, attrs); 853 keywords = getDataKeywords(context, attrs); 854 855 updateOneRowWithFilteredData(database, localeStr, title, summary, null, null, 856 fragmentName, screenTitle, iconResId, rank, 857 keywords, intentAction, intentTargetPackage, intentTargetClass, true, 858 key, -1 /* default user id */); 859 } 860 861 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 862 && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { 863 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 864 continue; 865 } 866 867 nodeName = parser.getName(); 868 869 key = getDataKey(context, attrs); 870 if (nonIndexableKeys.contains(key)) { 871 continue; 872 } 873 874 title = getDataTitle(context, attrs); 875 keywords = getDataKeywords(context, attrs); 876 877 if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { 878 summary = getDataSummary(context, attrs); 879 880 String entries = null; 881 882 if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { 883 entries = getDataEntries(context, attrs); 884 } 885 886 // Insert rows for the child nodes of PreferenceScreen 887 updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries, 888 fragmentName, screenTitle, iconResId, rank, 889 keywords, intentAction, intentTargetPackage, intentTargetClass, 890 true, key, -1 /* default user id */); 891 } else { 892 String summaryOn = getDataSummaryOn(context, attrs); 893 String summaryOff = getDataSummaryOff(context, attrs); 894 895 if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { 896 summaryOn = getDataSummary(context, attrs); 897 } 898 899 updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff, 900 null, fragmentName, screenTitle, iconResId, rank, 901 keywords, intentAction, intentTargetPackage, intentTargetClass, 902 true, key, -1 /* default user id */); 903 } 904 } 905 906 } catch (XmlPullParserException e) { 907 throw new RuntimeException("Error parsing PreferenceScreen", e); 908 } catch (IOException e) { 909 throw new RuntimeException("Error parsing PreferenceScreen", e); 910 } finally { 911 if (parser != null) parser.close(); 912 } 913 } 914 915 private void indexFromProvider(Context context, SQLiteDatabase database, String localeStr, 916 Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, 917 boolean enabled, List<String> nonIndexableKeys) { 918 919 if (provider == null) { 920 Log.w(LOG_TAG, "Cannot find provider: " + className); 921 return; 922 } 923 924 final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(context, enabled); 925 926 if (rawList != null) { 927 final int rawSize = rawList.size(); 928 for (int i = 0; i < rawSize; i++) { 929 SearchIndexableRaw raw = rawList.get(i); 930 931 // Should be the same locale as the one we are processing 932 if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { 933 continue; 934 } 935 936 if (nonIndexableKeys.contains(raw.key)) { 937 continue; 938 } 939 940 updateOneRowWithFilteredData(database, localeStr, 941 raw.title, 942 raw.summaryOn, 943 raw.summaryOff, 944 raw.entries, 945 className, 946 raw.screenTitle, 947 iconResId, 948 rank, 949 raw.keywords, 950 raw.intentAction, 951 raw.intentTargetPackage, 952 raw.intentTargetClass, 953 raw.enabled, 954 raw.key, 955 raw.userId); 956 } 957 } 958 959 final List<SearchIndexableResource> resList = 960 provider.getXmlResourcesToIndex(context, enabled); 961 if (resList != null) { 962 final int resSize = resList.size(); 963 for (int i = 0; i < resSize; i++) { 964 SearchIndexableResource item = resList.get(i); 965 966 // Should be the same locale as the one we are processing 967 if (!item.locale.toString().equalsIgnoreCase(localeStr)) { 968 continue; 969 } 970 971 final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId; 972 final int itemRank = (item.rank == 0) ? rank : item.rank; 973 String itemClassName = (TextUtils.isEmpty(item.className)) 974 ? className : item.className; 975 976 indexFromResource(context, database, localeStr, 977 item.xmlResId, itemClassName, itemIconResId, itemRank, 978 item.intentAction, item.intentTargetPackage, 979 item.intentTargetClass, nonIndexableKeys); 980 } 981 } 982 } 983 984 private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale, 985 String title, String summaryOn, String summaryOff, String entries, 986 String className, 987 String screenTitle, int iconResId, int rank, String keywords, 988 String intentAction, String intentTargetPackage, String intentTargetClass, 989 boolean enabled, String key, int userId) { 990 991 final String updatedTitle = normalizeHyphen(title); 992 final String updatedSummaryOn = normalizeHyphen(summaryOn); 993 final String updatedSummaryOff = normalizeHyphen(summaryOff); 994 995 final String normalizedTitle = normalizeString(updatedTitle); 996 final String normalizedSummaryOn = normalizeString(updatedSummaryOn); 997 final String normalizedSummaryOff = normalizeString(updatedSummaryOff); 998 999 updateOneRow(database, locale, 1000 updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn, 1001 updatedSummaryOff, normalizedSummaryOff, entries, 1002 className, screenTitle, iconResId, 1003 rank, keywords, intentAction, intentTargetPackage, intentTargetClass, enabled, 1004 key, userId); 1005 } 1006 1007 private static String normalizeHyphen(String input) { 1008 return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; 1009 } 1010 1011 private static String normalizeString(String input) { 1012 final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; 1013 final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); 1014 1015 return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); 1016 } 1017 1018 private void updateOneRow(SQLiteDatabase database, String locale, 1019 String updatedTitle, String normalizedTitle, 1020 String updatedSummaryOn, String normalizedSummaryOn, 1021 String updatedSummaryOff, String normalizedSummaryOff, String entries, 1022 String className, String screenTitle, int iconResId, int rank, String keywords, 1023 String intentAction, String intentTargetPackage, String intentTargetClass, 1024 boolean enabled, String key, int userId) { 1025 1026 if (TextUtils.isEmpty(updatedTitle)) { 1027 return; 1028 } 1029 1030 // The DocID should contains more than the title string itself (you may have two settings 1031 // with the same title). So we need to use a combination of the title and the screenTitle. 1032 StringBuilder sb = new StringBuilder(updatedTitle); 1033 sb.append(screenTitle); 1034 int docId = sb.toString().hashCode(); 1035 1036 ContentValues values = new ContentValues(); 1037 values.put(IndexColumns.DOCID, docId); 1038 values.put(IndexColumns.LOCALE, locale); 1039 values.put(IndexColumns.DATA_RANK, rank); 1040 values.put(IndexColumns.DATA_TITLE, updatedTitle); 1041 values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle); 1042 values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn); 1043 values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn); 1044 values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff); 1045 values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff); 1046 values.put(IndexColumns.DATA_ENTRIES, entries); 1047 values.put(IndexColumns.DATA_KEYWORDS, keywords); 1048 values.put(IndexColumns.CLASS_NAME, className); 1049 values.put(IndexColumns.SCREEN_TITLE, screenTitle); 1050 values.put(IndexColumns.INTENT_ACTION, intentAction); 1051 values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage); 1052 values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass); 1053 values.put(IndexColumns.ICON, iconResId); 1054 values.put(IndexColumns.ENABLED, enabled); 1055 values.put(IndexColumns.DATA_KEY_REF, key); 1056 values.put(IndexColumns.USER_ID, userId); 1057 1058 database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values); 1059 } 1060 1061 private String getDataKey(Context context, AttributeSet attrs) { 1062 return getData(context, attrs, 1063 com.android.internal.R.styleable.Preference, 1064 com.android.internal.R.styleable.Preference_key); 1065 } 1066 1067 private String getDataTitle(Context context, AttributeSet attrs) { 1068 return getData(context, attrs, 1069 com.android.internal.R.styleable.Preference, 1070 com.android.internal.R.styleable.Preference_title); 1071 } 1072 1073 private String getDataSummary(Context context, AttributeSet attrs) { 1074 return getData(context, attrs, 1075 com.android.internal.R.styleable.Preference, 1076 com.android.internal.R.styleable.Preference_summary); 1077 } 1078 1079 private String getDataSummaryOn(Context context, AttributeSet attrs) { 1080 return getData(context, attrs, 1081 com.android.internal.R.styleable.CheckBoxPreference, 1082 com.android.internal.R.styleable.CheckBoxPreference_summaryOn); 1083 } 1084 1085 private String getDataSummaryOff(Context context, AttributeSet attrs) { 1086 return getData(context, attrs, 1087 com.android.internal.R.styleable.CheckBoxPreference, 1088 com.android.internal.R.styleable.CheckBoxPreference_summaryOff); 1089 } 1090 1091 private String getDataEntries(Context context, AttributeSet attrs) { 1092 return getDataEntries(context, attrs, 1093 com.android.internal.R.styleable.ListPreference, 1094 com.android.internal.R.styleable.ListPreference_entries); 1095 } 1096 1097 private String getDataKeywords(Context context, AttributeSet attrs) { 1098 return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); 1099 } 1100 1101 private String getData(Context context, AttributeSet set, int[] attrs, int resId) { 1102 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1103 final TypedValue tv = sa.peekValue(resId); 1104 1105 CharSequence data = null; 1106 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 1107 if (tv.resourceId != 0) { 1108 data = context.getText(tv.resourceId); 1109 } else { 1110 data = tv.string; 1111 } 1112 } 1113 return (data != null) ? data.toString() : null; 1114 } 1115 1116 private String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) { 1117 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1118 final TypedValue tv = sa.peekValue(resId); 1119 1120 String[] data = null; 1121 if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { 1122 if (tv.resourceId != 0) { 1123 data = context.getResources().getStringArray(tv.resourceId); 1124 } 1125 } 1126 final int count = (data == null ) ? 0 : data.length; 1127 if (count == 0) { 1128 return null; 1129 } 1130 final StringBuilder result = new StringBuilder(); 1131 for (int n = 0; n < count; n++) { 1132 result.append(data[n]); 1133 result.append(ENTRIES_SEPARATOR); 1134 } 1135 return result.toString(); 1136 } 1137 1138 private int getResId(Context context, AttributeSet set, int[] attrs, int resId) { 1139 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1140 final TypedValue tv = sa.peekValue(resId); 1141 1142 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 1143 return tv.resourceId; 1144 } else { 1145 return 0; 1146 } 1147 } 1148 1149 /** 1150 * A private class for updating the Index database 1151 */ 1152 private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> { 1153 1154 @Override 1155 protected void onPreExecute() { 1156 super.onPreExecute(); 1157 mIsAvailable.set(false); 1158 } 1159 1160 @Override 1161 protected void onPostExecute(Void aVoid) { 1162 super.onPostExecute(aVoid); 1163 mIsAvailable.set(true); 1164 } 1165 1166 @Override 1167 protected Void doInBackground(UpdateData... params) { 1168 final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate; 1169 final List<SearchIndexableData> dataToDelete = params[0].dataToDelete; 1170 final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys; 1171 1172 final boolean forceUpdate = params[0].forceUpdate; 1173 1174 final SQLiteDatabase database = getWritableDatabase(); 1175 final String localeStr = Locale.getDefault().toString(); 1176 1177 try { 1178 database.beginTransaction(); 1179 if (dataToDelete.size() > 0) { 1180 processDataToDelete(database, localeStr, dataToDelete); 1181 } 1182 if (dataToUpdate.size() > 0) { 1183 processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, 1184 forceUpdate); 1185 } 1186 database.setTransactionSuccessful(); 1187 } finally { 1188 database.endTransaction(); 1189 } 1190 1191 return null; 1192 } 1193 1194 private boolean processDataToUpdate(SQLiteDatabase database, String localeStr, 1195 List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, 1196 boolean forceUpdate) { 1197 1198 if (!forceUpdate && isLocaleAlreadyIndexed(database, localeStr)) { 1199 Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed"); 1200 return true; 1201 } 1202 1203 boolean result = false; 1204 final long current = System.currentTimeMillis(); 1205 1206 final int count = dataToUpdate.size(); 1207 for (int n = 0; n < count; n++) { 1208 final SearchIndexableData data = dataToUpdate.get(n); 1209 try { 1210 indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys); 1211 } catch (Exception e) { 1212 Log.e(LOG_TAG, 1213 "Cannot index: " + data.className + " for locale: " + localeStr, e); 1214 } 1215 } 1216 1217 final long now = System.currentTimeMillis(); 1218 Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " + 1219 (now - current) + " millis"); 1220 return result; 1221 } 1222 1223 private boolean processDataToDelete(SQLiteDatabase database, String localeStr, 1224 List<SearchIndexableData> dataToDelete) { 1225 1226 boolean result = false; 1227 final long current = System.currentTimeMillis(); 1228 1229 final int count = dataToDelete.size(); 1230 for (int n = 0; n < count; n++) { 1231 final SearchIndexableData data = dataToDelete.get(n); 1232 if (data == null) { 1233 continue; 1234 } 1235 if (!TextUtils.isEmpty(data.className)) { 1236 delete(database, IndexColumns.CLASS_NAME, data.className); 1237 } else { 1238 if (data instanceof SearchIndexableRaw) { 1239 final SearchIndexableRaw raw = (SearchIndexableRaw) data; 1240 if (!TextUtils.isEmpty(raw.title)) { 1241 delete(database, IndexColumns.DATA_TITLE, raw.title); 1242 } 1243 } 1244 } 1245 } 1246 1247 final long now = System.currentTimeMillis(); 1248 Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " + 1249 (now - current) + " millis"); 1250 return result; 1251 } 1252 1253 private int delete(SQLiteDatabase database, String columName, String value) { 1254 final String whereClause = columName + "=?"; 1255 final String[] whereArgs = new String[] { value }; 1256 1257 return database.delete(Tables.TABLE_PREFS_INDEX, whereClause, whereArgs); 1258 } 1259 1260 private boolean isLocaleAlreadyIndexed(SQLiteDatabase database, String locale) { 1261 Cursor cursor = null; 1262 boolean result = false; 1263 final StringBuilder sb = new StringBuilder(IndexColumns.LOCALE); 1264 sb.append(" = "); 1265 DatabaseUtils.appendEscapedSQLString(sb, locale); 1266 try { 1267 // We care only for 1 row 1268 cursor = database.query(Tables.TABLE_PREFS_INDEX, null, 1269 sb.toString(), null, null, null, null, "1"); 1270 final int count = cursor.getCount(); 1271 result = (count >= 1); 1272 } finally { 1273 if (cursor != null) { 1274 cursor.close(); 1275 } 1276 } 1277 return result; 1278 } 1279 } 1280 1281 /** 1282 * A basic AsyncTask for saving a Search query into the database 1283 */ 1284 private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> { 1285 1286 @Override 1287 protected Long doInBackground(String... params) { 1288 final long now = new Date().getTime(); 1289 1290 final ContentValues values = new ContentValues(); 1291 values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]); 1292 values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now); 1293 1294 final SQLiteDatabase database = getWritableDatabase(); 1295 1296 long lastInsertedRowId = -1; 1297 try { 1298 // First, delete all saved queries that are the same 1299 database.delete(Tables.TABLE_SAVED_QUERIES, 1300 IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?", 1301 new String[] { params[0] }); 1302 1303 // Second, insert the saved query 1304 lastInsertedRowId = 1305 database.insertOrThrow(Tables.TABLE_SAVED_QUERIES, null, values); 1306 1307 // Last, remove "old" saved queries 1308 final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY; 1309 if (delta > 0) { 1310 int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?", 1311 new String[] { Long.toString(delta) }); 1312 Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)"); 1313 } 1314 } catch (Exception e) { 1315 Log.d(LOG_TAG, "Cannot update saved Search queries", e); 1316 } 1317 1318 return lastInsertedRowId; 1319 } 1320 } 1321} 1322