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