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