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