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