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 = false; 181 public boolean fullIndex = true; 182 UpdateData()183 public UpdateData() { 184 dataToUpdate = new ArrayList<SearchIndexableData>(); 185 dataToDelete = new ArrayList<SearchIndexableData>(); 186 nonIndexableKeys = new HashMap<String, List<String>>(); 187 } 188 UpdateData(UpdateData other)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 copy()197 public UpdateData copy() { 198 return new UpdateData(this); 199 } 200 clear()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 */ getInstance(Context context)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 Index(Context context, String baseAuthority)225 public Index(Context context, String baseAuthority) { 226 mContext = context; 227 mBaseAuthority = baseAuthority; 228 } 229 setContext(Context context)230 public void setContext(Context context) { 231 mContext = context; 232 } 233 isAvailable()234 public boolean isAvailable() { 235 return mIsAvailable.get(); 236 } 237 search(String query)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 getSuggestions(String query)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 buildSuggestionsSQL(String query)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 addSavedQuery(String query)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 update()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 addIndexablesFromRemoteProvider(String packageName, String authority)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 addNonIndexablesKeysFromRemoteProvider(String packageName, String authority)354 private void addNonIndexablesKeysFromRemoteProvider(String packageName, 355 String authority) { 356 final List<String> keys = 357 getNonIndexablesKeysFromRemoteProvider(packageName, authority); 358 addNonIndexableKeys(packageName, keys); 359 } 360 getNonIndexablesKeysFromRemoteProvider(String packageName, String authority)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 getNonIndexablesKeys(Context packageContext, Uri uri, String[] projection)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 addIndexableData(SearchIndexableData data)402 public void addIndexableData(SearchIndexableData data) { 403 synchronized (mDataToProcess) { 404 mDataToProcess.dataToUpdate.add(data); 405 } 406 } 407 addIndexableData(SearchIndexableResource[] array)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 deleteIndexableData(SearchIndexableData data)417 public void deleteIndexableData(SearchIndexableData data) { 418 synchronized (mDataToProcess) { 419 mDataToProcess.dataToDelete.add(data); 420 } 421 } 422 addNonIndexableKeys(String authority, List<String> keys)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 */ isWellKnownProvider(ResolveInfo info)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 isPrivilegedPackage(String packageName)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 updateFromRemoteProvider(String packageName, String authority)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 */ updateFromClassNameResource(String className, final boolean rebuild, boolean includeInSearchResults)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 updateFromSearchIndexableData(SearchIndexableData data)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 getReadableDatabase()520 private SQLiteDatabase getReadableDatabase() { 521 return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase(); 522 } 523 getWritableDatabase()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 buildUriForXmlResources(String authority)533 private static Uri buildUriForXmlResources(String authority) { 534 return Uri.parse("content://" + authority + "/" + 535 SearchIndexablesContract.INDEXABLES_XML_RES_PATH); 536 } 537 buildUriForRawData(String authority)538 private static Uri buildUriForRawData(String authority) { 539 return Uri.parse("content://" + authority + "/" + 540 SearchIndexablesContract.INDEXABLES_RAW_PATH); 541 } 542 buildUriForNonIndexableKeys(String authority)543 private static Uri buildUriForNonIndexableKeys(String authority) { 544 return Uri.parse("content://" + authority + "/" + 545 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); 546 } 547 updateInternal()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 addIndexablesForXmlResourceUri(Context packageContext, String packageName, Uri uri, String[] projection, int baseRank)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 addIndexablesForRawDataUri(Context packageContext, String packageName, Uri uri, String[] projection, int baseRank)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 buildSearchSQL(String query, String[] colums, boolean withOrderBy)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 buildSearchSQLForColumn(String query, String[] columnNames)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 buildSearchWhereStringForColumns(String query, String[] columnNames)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 buildSearchMatchStringForColumns(String query, String[] columnNames)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 indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, SearchIndexableData data, Map<String, List<String>> nonIndexableKeys)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 indexOneRaw(SQLiteDatabase database, String localeStr, SearchIndexableRaw raw)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 isIndexableClass(final Class<?> clazz)758 private static boolean isIndexableClass(final Class<?> clazz) { 759 return (clazz != null) && Indexable.class.isAssignableFrom(clazz); 760 } 761 getIndexableClass(String className)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 indexOneResource(SQLiteDatabase database, String localeStr, SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource)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 getSearchIndexProvider(final Class<?> clazz)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 indexFromResource(Context context, SQLiteDatabase database, String localeStr, int xmlResId, String fragmentName, int iconResId, int rank, String intentAction, String intentTargetPackage, String intentTargetClass, List<String> nonIndexableKeys)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 indexFromProvider(Context context, SQLiteDatabase database, String localeStr, Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, boolean enabled, List<String> nonIndexableKeys)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 updateOneRowWithFilteredData(SQLiteDatabase database, String locale, String title, String summaryOn, String summaryOff, String entries, String className, String screenTitle, int iconResId, int rank, String keywords, String intentAction, String intentTargetPackage, String intentTargetClass, boolean enabled, String key, int userId)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 normalizeHyphen(String input)1034 private static String normalizeHyphen(String input) { 1035 return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; 1036 } 1037 normalizeString(String input)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 normalizeKeywords(String input)1045 private static String normalizeKeywords(String input) { 1046 return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; 1047 } 1048 updateOneRow(SQLiteDatabase database, String locale, String updatedTitle, String normalizedTitle, String updatedSummaryOn, String normalizedSummaryOn, String updatedSummaryOff, String normalizedSummaryOff, String entries, String className, String screenTitle, int iconResId, int rank, String spaceDelimitedKeywords, String intentAction, String intentTargetPackage, String intentTargetClass, boolean enabled, String key, int userId)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 getDataKey(Context context, AttributeSet attrs)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 getDataTitle(Context context, AttributeSet attrs)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 getDataSummary(Context context, AttributeSet attrs)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 getDataSummaryOn(Context context, AttributeSet attrs)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 getDataSummaryOff(Context context, AttributeSet attrs)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 getDataEntries(Context context, AttributeSet attrs)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 getDataKeywords(Context context, AttributeSet attrs)1127 private String getDataKeywords(Context context, AttributeSet attrs) { 1128 return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); 1129 } 1130 getData(Context context, AttributeSet set, int[] attrs, int resId)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 getDataEntries(Context context, AttributeSet set, int[] attrs, int resId)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 getResId(Context context, AttributeSet set, int[] attrs, int resId)1168 private int getResId(Context context, AttributeSet set, int[] attrs, int resId) { 1169 final TypedArray sa = context.obtainStyledAttributes(set, attrs); 1170 final TypedValue tv = sa.peekValue(resId); 1171 1172 if (tv != null && tv.type == TypedValue.TYPE_STRING) { 1173 return tv.resourceId; 1174 } else { 1175 return 0; 1176 } 1177 } 1178 1179 /** 1180 * A private class for updating the Index database 1181 */ 1182 private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> { 1183 1184 @Override onPreExecute()1185 protected void onPreExecute() { 1186 super.onPreExecute(); 1187 mIsAvailable.set(false); 1188 } 1189 1190 @Override onPostExecute(Void aVoid)1191 protected void onPostExecute(Void aVoid) { 1192 super.onPostExecute(aVoid); 1193 mIsAvailable.set(true); 1194 } 1195 1196 @Override doInBackground(UpdateData... params)1197 protected Void doInBackground(UpdateData... params) { 1198 try { 1199 final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate; 1200 final List<SearchIndexableData> dataToDelete = params[0].dataToDelete; 1201 final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys; 1202 1203 final boolean forceUpdate = params[0].forceUpdate; 1204 final boolean fullIndex = params[0].fullIndex; 1205 1206 final SQLiteDatabase database = getWritableDatabase(); 1207 if (database == null) { 1208 Log.e(LOG_TAG, "Cannot update Index as I cannot get a writable database"); 1209 return null; 1210 } 1211 final String localeStr = Locale.getDefault().toString(); 1212 1213 try { 1214 database.beginTransaction(); 1215 if (dataToDelete.size() > 0) { 1216 processDataToDelete(database, localeStr, dataToDelete); 1217 } 1218 if (dataToUpdate.size() > 0) { 1219 processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, 1220 forceUpdate); 1221 } 1222 database.setTransactionSuccessful(); 1223 } finally { 1224 database.endTransaction(); 1225 } 1226 if (fullIndex) { 1227 IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr); 1228 } 1229 } catch (SQLiteFullException e) { 1230 Log.e(LOG_TAG, "Unable to index search, out of space", e); 1231 } 1232 1233 return null; 1234 } 1235 processDataToUpdate(SQLiteDatabase database, String localeStr, List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, boolean forceUpdate)1236 private boolean processDataToUpdate(SQLiteDatabase database, String localeStr, 1237 List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys, 1238 boolean forceUpdate) { 1239 1240 if (!forceUpdate && IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)) { 1241 Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed"); 1242 return true; 1243 } 1244 1245 boolean result = false; 1246 final long current = System.currentTimeMillis(); 1247 1248 final int count = dataToUpdate.size(); 1249 for (int n = 0; n < count; n++) { 1250 final SearchIndexableData data = dataToUpdate.get(n); 1251 try { 1252 indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys); 1253 } catch (Exception e) { 1254 Log.e(LOG_TAG, "Cannot index: " + (data != null ? data.className : data) 1255 + " for locale: " + localeStr, e); 1256 } 1257 } 1258 1259 final long now = System.currentTimeMillis(); 1260 Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " + 1261 (now - current) + " millis"); 1262 return result; 1263 } 1264 processDataToDelete(SQLiteDatabase database, String localeStr, List<SearchIndexableData> dataToDelete)1265 private boolean processDataToDelete(SQLiteDatabase database, String localeStr, 1266 List<SearchIndexableData> dataToDelete) { 1267 1268 boolean result = false; 1269 final long current = System.currentTimeMillis(); 1270 1271 final int count = dataToDelete.size(); 1272 for (int n = 0; n < count; n++) { 1273 final SearchIndexableData data = dataToDelete.get(n); 1274 if (data == null) { 1275 continue; 1276 } 1277 if (!TextUtils.isEmpty(data.className)) { 1278 delete(database, IndexColumns.CLASS_NAME, data.className); 1279 } else { 1280 if (data instanceof SearchIndexableRaw) { 1281 final SearchIndexableRaw raw = (SearchIndexableRaw) data; 1282 if (!TextUtils.isEmpty(raw.title)) { 1283 delete(database, IndexColumns.DATA_TITLE, raw.title); 1284 } 1285 } 1286 } 1287 } 1288 1289 final long now = System.currentTimeMillis(); 1290 Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " + 1291 (now - current) + " millis"); 1292 return result; 1293 } 1294 delete(SQLiteDatabase database, String columName, String value)1295 private int delete(SQLiteDatabase database, String columName, String value) { 1296 final String whereClause = columName + "=?"; 1297 final String[] whereArgs = new String[] { value }; 1298 1299 return database.delete(Tables.TABLE_PREFS_INDEX, whereClause, whereArgs); 1300 } 1301 } 1302 1303 /** 1304 * A basic AsyncTask for saving a Search query into the database 1305 */ 1306 private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> { 1307 1308 @Override doInBackground(String... params)1309 protected Long doInBackground(String... params) { 1310 final long now = new Date().getTime(); 1311 1312 final ContentValues values = new ContentValues(); 1313 values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]); 1314 values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now); 1315 1316 final SQLiteDatabase database = getWritableDatabase(); 1317 if (database == null) { 1318 Log.e(LOG_TAG, "Cannot save Search queries as I cannot get a writable database"); 1319 return -1L; 1320 } 1321 1322 long lastInsertedRowId = -1L; 1323 try { 1324 // First, delete all saved queries that are the same 1325 database.delete(Tables.TABLE_SAVED_QUERIES, 1326 IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?", 1327 new String[] { params[0] }); 1328 1329 // Second, insert the saved query 1330 lastInsertedRowId = 1331 database.insertOrThrow(Tables.TABLE_SAVED_QUERIES, null, values); 1332 1333 // Last, remove "old" saved queries 1334 final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY; 1335 if (delta > 0) { 1336 int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?", 1337 new String[] { Long.toString(delta) }); 1338 Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)"); 1339 } 1340 } catch (Exception e) { 1341 Log.d(LOG_TAG, "Cannot update saved Search queries", e); 1342 } 1343 1344 return lastInsertedRowId; 1345 } 1346 } 1347 } 1348