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