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