Index.java revision cc254f4da96506901268c4a0b1d3cfacb5f44948
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.settings.search;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.content.Intent;
23import android.content.pm.ApplicationInfo;
24import android.content.pm.PackageInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.TypedArray;
28import android.content.res.XmlResourceParser;
29import android.database.Cursor;
30import android.database.DatabaseUtils;
31import android.database.MergeCursor;
32import android.database.sqlite.SQLiteDatabase;
33import android.net.Uri;
34import android.os.AsyncTask;
35import android.provider.SearchIndexableData;
36import android.provider.SearchIndexableResource;
37import android.provider.SearchIndexablesContract;
38import android.text.TextUtils;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.util.TypedValue;
42import android.util.Xml;
43import com.android.settings.R;
44import org.xmlpull.v1.XmlPullParser;
45import org.xmlpull.v1.XmlPullParserException;
46
47import java.io.IOException;
48import java.lang.reflect.Field;
49import java.text.Normalizer;
50import java.util.ArrayList;
51import java.util.Collections;
52import java.util.Date;
53import java.util.HashMap;
54import java.util.List;
55import java.util.Locale;
56import java.util.Map;
57import java.util.concurrent.ExecutionException;
58import java.util.concurrent.atomic.AtomicBoolean;
59import java.util.regex.Pattern;
60
61import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
62import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK;
63import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
64import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
65import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
66import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
67import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
68import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
69import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
70import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
71import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
72import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
73import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
74import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
75import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
76
77import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RANK;
78import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
79import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
80import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
81import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
82import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
83import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
84
85import static com.android.settings.search.IndexDatabaseHelper.Tables;
86import static com.android.settings.search.IndexDatabaseHelper.IndexColumns;
87
88public 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
178        public UpdateData() {
179            dataToUpdate = new ArrayList<SearchIndexableData>();
180            dataToDelete = new ArrayList<SearchIndexableData>();
181            nonIndexableKeys = new HashMap<String, List<String>>();
182        }
183
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
191        public UpdateData copy() {
192            return new UpdateData(this);
193        }
194
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     */
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
220    public Index(Context context, String baseAuthority) {
221        mContext = context;
222        mBaseAuthority = baseAuthority;
223    }
224
225    public void setContext(Context context) {
226        mContext = context;
227    }
228
229    public boolean isAvailable() {
230        return mIsAvailable.get();
231    }
232
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
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
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
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
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
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
343    private void addNonIndexablesKeysFromRemoteProvider(String packageName,
344                                                        String authority) {
345        final List<String> keys =
346                getNonIndexablesKeysFromRemoteProvider(packageName, authority);
347        addNonIndexableKeys(packageName, keys);
348    }
349
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
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
391    public void addIndexableData(SearchIndexableData data) {
392        synchronized (mDataToProcess) {
393            mDataToProcess.dataToUpdate.add(data);
394        }
395    }
396
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
406    public void deleteIndexableData(SearchIndexableData data) {
407        synchronized (mDataToProcess) {
408            mDataToProcess.dataToDelete.add(data);
409        }
410    }
411
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     */
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
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.privateFlags
452                & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0);
453        } catch (PackageManager.NameNotFoundException e) {
454            return false;
455        }
456    }
457
458    private void updateFromRemoteProvider(String packageName, String authority) {
459        if (addIndexablesFromRemoteProvider(packageName, authority)) {
460            updateInternal();
461        }
462    }
463
464    /**
465     * Update the Index for a specific class name resources
466     *
467     * @param className the class name (typically a fragment name).
468     * @param rebuild true means that you want to delete the data from the Index first.
469     * @param includeInSearchResults true means that you want the bit "enabled" set so that the
470     *                               data will be seen included into the search results
471     */
472    public void updateFromClassNameResource(String className, boolean rebuild,
473            boolean includeInSearchResults) {
474        if (className == null) {
475            throw new IllegalArgumentException("class name cannot be null!");
476        }
477        final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className);
478        if (res == null ) {
479            Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className);
480            return;
481        }
482        res.context = mContext;
483        res.enabled = includeInSearchResults;
484        if (rebuild) {
485            deleteIndexableData(res);
486        }
487        addIndexableData(res);
488        mDataToProcess.forceUpdate = true;
489        updateInternal();
490        res.enabled = false;
491    }
492
493    public void updateFromSearchIndexableData(SearchIndexableData data) {
494        addIndexableData(data);
495        mDataToProcess.forceUpdate = true;
496        updateInternal();
497    }
498
499    private SQLiteDatabase getReadableDatabase() {
500        return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
501    }
502
503    private SQLiteDatabase getWritableDatabase() {
504        return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
505    }
506
507    private static Uri buildUriForXmlResources(String authority) {
508        return Uri.parse("content://" + authority + "/" +
509                SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
510    }
511
512    private static Uri buildUriForRawData(String authority) {
513        return Uri.parse("content://" + authority + "/" +
514                SearchIndexablesContract.INDEXABLES_RAW_PATH);
515    }
516
517    private static Uri buildUriForNonIndexableKeys(String authority) {
518        return Uri.parse("content://" + authority + "/" +
519                SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
520    }
521
522    private void updateInternal() {
523        synchronized (mDataToProcess) {
524            final UpdateIndexTask task = new UpdateIndexTask();
525            UpdateData copy = mDataToProcess.copy();
526            task.execute(copy);
527            mDataToProcess.clear();
528        }
529    }
530
531    private void addIndexablesForXmlResourceUri(Context packageContext, String packageName,
532            Uri uri, String[] projection, int baseRank) {
533
534        final ContentResolver resolver = packageContext.getContentResolver();
535        final Cursor cursor = resolver.query(uri, projection, null, null, null);
536
537        if (cursor == null) {
538            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
539            return;
540        }
541
542        try {
543            final int count = cursor.getCount();
544            if (count > 0) {
545                while (cursor.moveToNext()) {
546                    final int providerRank = cursor.getInt(COLUMN_INDEX_XML_RES_RANK);
547                    final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;
548
549                    final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
550
551                    final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
552                    final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
553
554                    final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
555                    final String targetPackage = cursor.getString(
556                            COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
557                    final String targetClass = cursor.getString(
558                            COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
559
560                    SearchIndexableResource sir = new SearchIndexableResource(packageContext);
561                    sir.rank = rank;
562                    sir.xmlResId = xmlResId;
563                    sir.className = className;
564                    sir.packageName = packageName;
565                    sir.iconResId = iconResId;
566                    sir.intentAction = action;
567                    sir.intentTargetPackage = targetPackage;
568                    sir.intentTargetClass = targetClass;
569
570                    addIndexableData(sir);
571                }
572            }
573        } finally {
574            cursor.close();
575        }
576    }
577
578    private void addIndexablesForRawDataUri(Context packageContext, String packageName,
579            Uri uri, String[] projection, int baseRank) {
580
581        final ContentResolver resolver = packageContext.getContentResolver();
582        final Cursor cursor = resolver.query(uri, projection, null, null, null);
583
584        if (cursor == null) {
585            Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString());
586            return;
587        }
588
589        try {
590            final int count = cursor.getCount();
591            if (count > 0) {
592                while (cursor.moveToNext()) {
593                    final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK);
594                    final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank;
595
596                    final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
597                    final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
598                    final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
599                    final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
600                    final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
601
602                    final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
603
604                    final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
605                    final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
606
607                    final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
608                    final String targetPackage = cursor.getString(
609                            COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
610                    final String targetClass = cursor.getString(
611                            COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
612
613                    final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
614                    final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
615
616                    SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
617                    data.rank = rank;
618                    data.title = title;
619                    data.summaryOn = summaryOn;
620                    data.summaryOff = summaryOff;
621                    data.entries = entries;
622                    data.keywords = keywords;
623                    data.screenTitle = screenTitle;
624                    data.className = className;
625                    data.packageName = packageName;
626                    data.iconResId = iconResId;
627                    data.intentAction = action;
628                    data.intentTargetPackage = targetPackage;
629                    data.intentTargetClass = targetClass;
630                    data.key = key;
631                    data.userId = userId;
632
633                    addIndexableData(data);
634                }
635            }
636        } finally {
637            cursor.close();
638        }
639    }
640
641    private String buildSearchSQL(String query, String[] colums, boolean withOrderBy) {
642        StringBuilder sb = new StringBuilder();
643        sb.append(buildSearchSQLForColumn(query, colums));
644        if (withOrderBy) {
645            sb.append(" ORDER BY ");
646            sb.append(IndexColumns.DATA_RANK);
647        }
648        return sb.toString();
649    }
650
651    private String buildSearchSQLForColumn(String query, String[] columnNames) {
652        StringBuilder sb = new StringBuilder();
653        sb.append("SELECT ");
654        for (int n = 0; n < SELECT_COLUMNS.length; n++) {
655            sb.append(SELECT_COLUMNS[n]);
656            if (n < SELECT_COLUMNS.length - 1) {
657                sb.append(", ");
658            }
659        }
660        sb.append(" FROM ");
661        sb.append(Tables.TABLE_PREFS_INDEX);
662        sb.append(" WHERE ");
663        sb.append(buildSearchWhereStringForColumns(query, columnNames));
664
665        return sb.toString();
666    }
667
668    private String buildSearchWhereStringForColumns(String query, String[] columnNames) {
669        final StringBuilder sb = new StringBuilder(Tables.TABLE_PREFS_INDEX);
670        sb.append(" MATCH ");
671        DatabaseUtils.appendEscapedSQLString(sb,
672                buildSearchMatchStringForColumns(query, columnNames));
673        sb.append(" AND ");
674        sb.append(IndexColumns.LOCALE);
675        sb.append(" = ");
676        DatabaseUtils.appendEscapedSQLString(sb, Locale.getDefault().toString());
677        sb.append(" AND ");
678        sb.append(IndexColumns.ENABLED);
679        sb.append(" = 1");
680        return sb.toString();
681    }
682
683    private String buildSearchMatchStringForColumns(String query, String[] columnNames) {
684        final String value = query + "*";
685        StringBuilder sb = new StringBuilder();
686        final int count = columnNames.length;
687        for (int n = 0; n < count; n++) {
688            sb.append(columnNames[n]);
689            sb.append(":");
690            sb.append(value);
691            if (n < count - 1) {
692                sb.append(" OR ");
693            }
694        }
695        return sb.toString();
696    }
697
698    private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr,
699            SearchIndexableData data, Map<String, List<String>> nonIndexableKeys) {
700        if (data instanceof SearchIndexableResource) {
701            indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys);
702        } else if (data instanceof SearchIndexableRaw) {
703            indexOneRaw(database, localeStr, (SearchIndexableRaw) data);
704        }
705    }
706
707    private void indexOneRaw(SQLiteDatabase database, String localeStr,
708                             SearchIndexableRaw raw) {
709        // Should be the same locale as the one we are processing
710        if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
711            return;
712        }
713
714        updateOneRowWithFilteredData(database, localeStr,
715                raw.title,
716                raw.summaryOn,
717                raw.summaryOff,
718                raw.entries,
719                raw.className,
720                raw.screenTitle,
721                raw.iconResId,
722                raw.rank,
723                raw.keywords,
724                raw.intentAction,
725                raw.intentTargetPackage,
726                raw.intentTargetClass,
727                raw.enabled,
728                raw.key,
729                raw.userId);
730    }
731
732    private static boolean isIndexableClass(final Class<?> clazz) {
733        return (clazz != null) && Indexable.class.isAssignableFrom(clazz);
734    }
735
736    private static Class<?> getIndexableClass(String className) {
737        final Class<?> clazz;
738        try {
739            clazz = Class.forName(className);
740        } catch (ClassNotFoundException e) {
741            Log.d(LOG_TAG, "Cannot find class: " + className);
742            return null;
743        }
744        return isIndexableClass(clazz) ? clazz : null;
745    }
746
747    private void indexOneResource(SQLiteDatabase database, String localeStr,
748            SearchIndexableResource sir, Map<String, List<String>> nonIndexableKeysFromResource) {
749
750        if (sir == null) {
751            Log.e(LOG_TAG, "Cannot index a null resource!");
752            return;
753        }
754
755        final List<String> nonIndexableKeys = new ArrayList<String>();
756
757        if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) {
758            List<String> resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName);
759            if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) {
760                nonIndexableKeys.addAll(resNonIndxableKeys);
761            }
762
763            indexFromResource(sir.context, database, localeStr,
764                    sir.xmlResId, sir.className, sir.iconResId, sir.rank,
765                    sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass,
766                    nonIndexableKeys);
767        } else {
768            if (TextUtils.isEmpty(sir.className)) {
769                Log.w(LOG_TAG, "Cannot index an empty Search Provider name!");
770                return;
771            }
772
773            final Class<?> clazz = getIndexableClass(sir.className);
774            if (clazz == null) {
775                Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className +
776                        "' should implement the " + Indexable.class.getName() + " interface!");
777                return;
778            }
779
780            // Will be non null only for a Local provider implementing a
781            // SEARCH_INDEX_DATA_PROVIDER field
782            final Indexable.SearchIndexProvider provider = getSearchIndexProvider(clazz);
783            if (provider != null) {
784                List<String> providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context);
785                if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) {
786                    nonIndexableKeys.addAll(providerNonIndexableKeys);
787                }
788
789                indexFromProvider(mContext, database, localeStr, provider, sir.className,
790                        sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys);
791            }
792        }
793    }
794
795    private Indexable.SearchIndexProvider getSearchIndexProvider(final Class<?> clazz) {
796        try {
797            final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER);
798            return (Indexable.SearchIndexProvider) f.get(null);
799        } catch (NoSuchFieldException e) {
800            Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
801        } catch (SecurityException se) {
802            Log.d(LOG_TAG,
803                    "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
804        } catch (IllegalAccessException e) {
805            Log.d(LOG_TAG,
806                    "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
807        } catch (IllegalArgumentException e) {
808            Log.d(LOG_TAG,
809                    "Illegal argument when accessing field '" +
810                            FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'");
811        }
812        return null;
813    }
814
815    private void indexFromResource(Context context, SQLiteDatabase database, String localeStr,
816           int xmlResId, String fragmentName, int iconResId, int rank,
817           String intentAction, String intentTargetPackage, String intentTargetClass,
818           List<String> nonIndexableKeys) {
819
820        XmlResourceParser parser = null;
821        try {
822            parser = context.getResources().getXml(xmlResId);
823
824            int type;
825            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
826                    && type != XmlPullParser.START_TAG) {
827                // Parse next until start tag is found
828            }
829
830            String nodeName = parser.getName();
831            if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
832                throw new RuntimeException(
833                        "XML document must start with <PreferenceScreen> tag; found"
834                                + nodeName + " at " + parser.getPositionDescription());
835            }
836
837            final int outerDepth = parser.getDepth();
838            final AttributeSet attrs = Xml.asAttributeSet(parser);
839
840            final String screenTitle = getDataTitle(context, attrs);
841
842            String key = getDataKey(context, attrs);
843
844            String title;
845            String summary;
846            String keywords;
847
848            // Insert rows for the main PreferenceScreen node. Rewrite the data for removing
849            // hyphens.
850            if (!nonIndexableKeys.contains(key)) {
851                title = getDataTitle(context, attrs);
852                summary = getDataSummary(context, attrs);
853                keywords = getDataKeywords(context, attrs);
854
855                updateOneRowWithFilteredData(database, localeStr, title, summary, null, null,
856                        fragmentName, screenTitle, iconResId, rank,
857                        keywords, intentAction, intentTargetPackage, intentTargetClass, true,
858                        key, -1 /* default user id */);
859            }
860
861            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
862                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
863                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
864                    continue;
865                }
866
867                nodeName = parser.getName();
868
869                key = getDataKey(context, attrs);
870                if (nonIndexableKeys.contains(key)) {
871                    continue;
872                }
873
874                title = getDataTitle(context, attrs);
875                keywords = getDataKeywords(context, attrs);
876
877                if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
878                    summary = getDataSummary(context, attrs);
879
880                    String entries = null;
881
882                    if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
883                        entries = getDataEntries(context, attrs);
884                    }
885
886                    // Insert rows for the child nodes of PreferenceScreen
887                    updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries,
888                            fragmentName, screenTitle, iconResId, rank,
889                            keywords, intentAction, intentTargetPackage, intentTargetClass,
890                            true, key, -1 /* default user id */);
891                } else {
892                    String summaryOn = getDataSummaryOn(context, attrs);
893                    String summaryOff = getDataSummaryOff(context, attrs);
894
895                    if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) {
896                        summaryOn = getDataSummary(context, attrs);
897                    }
898
899                    updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff,
900                            null, fragmentName, screenTitle, iconResId, rank,
901                            keywords, intentAction, intentTargetPackage, intentTargetClass,
902                            true, key, -1 /* default user id */);
903                }
904            }
905
906        } catch (XmlPullParserException e) {
907            throw new RuntimeException("Error parsing PreferenceScreen", e);
908        } catch (IOException e) {
909            throw new RuntimeException("Error parsing PreferenceScreen", e);
910        } finally {
911            if (parser != null) parser.close();
912        }
913    }
914
915    private void indexFromProvider(Context context, SQLiteDatabase database, String localeStr,
916            Indexable.SearchIndexProvider provider, String className, int iconResId, int rank,
917            boolean enabled, List<String> nonIndexableKeys) {
918
919        if (provider == null) {
920            Log.w(LOG_TAG, "Cannot find provider: " + className);
921            return;
922        }
923
924        final List<SearchIndexableRaw> rawList = provider.getRawDataToIndex(context, enabled);
925
926        if (rawList != null) {
927            final int rawSize = rawList.size();
928            for (int i = 0; i < rawSize; i++) {
929                SearchIndexableRaw raw = rawList.get(i);
930
931                // Should be the same locale as the one we are processing
932                if (!raw.locale.toString().equalsIgnoreCase(localeStr)) {
933                    continue;
934                }
935
936                if (nonIndexableKeys.contains(raw.key)) {
937                    continue;
938                }
939
940                updateOneRowWithFilteredData(database, localeStr,
941                        raw.title,
942                        raw.summaryOn,
943                        raw.summaryOff,
944                        raw.entries,
945                        className,
946                        raw.screenTitle,
947                        iconResId,
948                        rank,
949                        raw.keywords,
950                        raw.intentAction,
951                        raw.intentTargetPackage,
952                        raw.intentTargetClass,
953                        raw.enabled,
954                        raw.key,
955                        raw.userId);
956            }
957        }
958
959        final List<SearchIndexableResource> resList =
960                provider.getXmlResourcesToIndex(context, enabled);
961        if (resList != null) {
962            final int resSize = resList.size();
963            for (int i = 0; i < resSize; i++) {
964                SearchIndexableResource item = resList.get(i);
965
966                // Should be the same locale as the one we are processing
967                if (!item.locale.toString().equalsIgnoreCase(localeStr)) {
968                    continue;
969                }
970
971                final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId;
972                final int itemRank = (item.rank == 0) ? rank : item.rank;
973                String itemClassName = (TextUtils.isEmpty(item.className))
974                        ? className : item.className;
975
976                indexFromResource(context, database, localeStr,
977                        item.xmlResId, itemClassName, itemIconResId, itemRank,
978                        item.intentAction, item.intentTargetPackage,
979                        item.intentTargetClass, nonIndexableKeys);
980            }
981        }
982    }
983
984    private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale,
985            String title, String summaryOn, String summaryOff, String entries,
986            String className,
987            String screenTitle, int iconResId, int rank, String keywords,
988            String intentAction, String intentTargetPackage, String intentTargetClass,
989            boolean enabled, String key, int userId) {
990
991        final String updatedTitle = normalizeHyphen(title);
992        final String updatedSummaryOn = normalizeHyphen(summaryOn);
993        final String updatedSummaryOff = normalizeHyphen(summaryOff);
994
995        final String normalizedTitle = normalizeString(updatedTitle);
996        final String normalizedSummaryOn = normalizeString(updatedSummaryOn);
997        final String normalizedSummaryOff = normalizeString(updatedSummaryOff);
998
999        updateOneRow(database, locale,
1000                updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn,
1001                updatedSummaryOff, normalizedSummaryOff, entries,
1002                className, screenTitle, iconResId,
1003                rank, keywords, intentAction, intentTargetPackage, intentTargetClass, enabled,
1004                key, userId);
1005    }
1006
1007    private static String normalizeHyphen(String input) {
1008        return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY;
1009    }
1010
1011    private static String normalizeString(String input) {
1012        final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY;
1013        final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD);
1014
1015        return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase();
1016    }
1017
1018    private void updateOneRow(SQLiteDatabase database, String locale,
1019            String updatedTitle, String normalizedTitle,
1020            String updatedSummaryOn, String normalizedSummaryOn,
1021            String updatedSummaryOff, String normalizedSummaryOff, String entries,
1022            String className, String screenTitle, int iconResId, int rank, String keywords,
1023            String intentAction, String intentTargetPackage, String intentTargetClass,
1024            boolean enabled, String key, int userId) {
1025
1026        if (TextUtils.isEmpty(updatedTitle)) {
1027            return;
1028        }
1029
1030        // The DocID should contains more than the title string itself (you may have two settings
1031        // with the same title). So we need to use a combination of the title and the screenTitle.
1032        StringBuilder sb = new StringBuilder(updatedTitle);
1033        sb.append(screenTitle);
1034        int docId = sb.toString().hashCode();
1035
1036        ContentValues values = new ContentValues();
1037        values.put(IndexColumns.DOCID, docId);
1038        values.put(IndexColumns.LOCALE, locale);
1039        values.put(IndexColumns.DATA_RANK, rank);
1040        values.put(IndexColumns.DATA_TITLE, updatedTitle);
1041        values.put(IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle);
1042        values.put(IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn);
1043        values.put(IndexColumns.DATA_SUMMARY_ON_NORMALIZED, normalizedSummaryOn);
1044        values.put(IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff);
1045        values.put(IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, normalizedSummaryOff);
1046        values.put(IndexColumns.DATA_ENTRIES, entries);
1047        values.put(IndexColumns.DATA_KEYWORDS, keywords);
1048        values.put(IndexColumns.CLASS_NAME, className);
1049        values.put(IndexColumns.SCREEN_TITLE, screenTitle);
1050        values.put(IndexColumns.INTENT_ACTION, intentAction);
1051        values.put(IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage);
1052        values.put(IndexColumns.INTENT_TARGET_CLASS, intentTargetClass);
1053        values.put(IndexColumns.ICON, iconResId);
1054        values.put(IndexColumns.ENABLED, enabled);
1055        values.put(IndexColumns.DATA_KEY_REF, key);
1056        values.put(IndexColumns.USER_ID, userId);
1057
1058        database.replaceOrThrow(Tables.TABLE_PREFS_INDEX, null, values);
1059    }
1060
1061    private String getDataKey(Context context, AttributeSet attrs) {
1062        return getData(context, attrs,
1063                com.android.internal.R.styleable.Preference,
1064                com.android.internal.R.styleable.Preference_key);
1065    }
1066
1067    private String getDataTitle(Context context, AttributeSet attrs) {
1068        return getData(context, attrs,
1069                com.android.internal.R.styleable.Preference,
1070                com.android.internal.R.styleable.Preference_title);
1071    }
1072
1073    private String getDataSummary(Context context, AttributeSet attrs) {
1074        return getData(context, attrs,
1075                com.android.internal.R.styleable.Preference,
1076                com.android.internal.R.styleable.Preference_summary);
1077    }
1078
1079    private String getDataSummaryOn(Context context, AttributeSet attrs) {
1080        return getData(context, attrs,
1081                com.android.internal.R.styleable.CheckBoxPreference,
1082                com.android.internal.R.styleable.CheckBoxPreference_summaryOn);
1083    }
1084
1085    private String getDataSummaryOff(Context context, AttributeSet attrs) {
1086        return getData(context, attrs,
1087                com.android.internal.R.styleable.CheckBoxPreference,
1088                com.android.internal.R.styleable.CheckBoxPreference_summaryOff);
1089    }
1090
1091    private String getDataEntries(Context context, AttributeSet attrs) {
1092        return getDataEntries(context, attrs,
1093                com.android.internal.R.styleable.ListPreference,
1094                com.android.internal.R.styleable.ListPreference_entries);
1095    }
1096
1097    private String getDataKeywords(Context context, AttributeSet attrs) {
1098        return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords);
1099    }
1100
1101    private String getData(Context context, AttributeSet set, int[] attrs, int resId) {
1102        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
1103        final TypedValue tv = sa.peekValue(resId);
1104
1105        CharSequence data = null;
1106        if (tv != null && tv.type == TypedValue.TYPE_STRING) {
1107            if (tv.resourceId != 0) {
1108                data = context.getText(tv.resourceId);
1109            } else {
1110                data = tv.string;
1111            }
1112        }
1113        return (data != null) ? data.toString() : null;
1114    }
1115
1116    private String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) {
1117        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
1118        final TypedValue tv = sa.peekValue(resId);
1119
1120        String[] data = null;
1121        if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) {
1122            if (tv.resourceId != 0) {
1123                data = context.getResources().getStringArray(tv.resourceId);
1124            }
1125        }
1126        final int count = (data == null ) ? 0 : data.length;
1127        if (count == 0) {
1128            return null;
1129        }
1130        final StringBuilder result = new StringBuilder();
1131        for (int n = 0; n < count; n++) {
1132            result.append(data[n]);
1133            result.append(ENTRIES_SEPARATOR);
1134        }
1135        return result.toString();
1136    }
1137
1138    private int getResId(Context context, AttributeSet set, int[] attrs, int resId) {
1139        final TypedArray sa = context.obtainStyledAttributes(set, attrs);
1140        final TypedValue tv = sa.peekValue(resId);
1141
1142        if (tv != null && tv.type == TypedValue.TYPE_STRING) {
1143            return tv.resourceId;
1144        } else {
1145            return 0;
1146        }
1147   }
1148
1149    /**
1150     * A private class for updating the Index database
1151     */
1152    private class UpdateIndexTask extends AsyncTask<UpdateData, Integer, Void> {
1153
1154        @Override
1155        protected void onPreExecute() {
1156            super.onPreExecute();
1157            mIsAvailable.set(false);
1158        }
1159
1160        @Override
1161        protected void onPostExecute(Void aVoid) {
1162            super.onPostExecute(aVoid);
1163            mIsAvailable.set(true);
1164        }
1165
1166        @Override
1167        protected Void doInBackground(UpdateData... params) {
1168            final List<SearchIndexableData> dataToUpdate = params[0].dataToUpdate;
1169            final List<SearchIndexableData> dataToDelete = params[0].dataToDelete;
1170            final Map<String, List<String>> nonIndexableKeys = params[0].nonIndexableKeys;
1171
1172            final boolean forceUpdate = params[0].forceUpdate;
1173
1174            final SQLiteDatabase database = getWritableDatabase();
1175            final String localeStr = Locale.getDefault().toString();
1176
1177            try {
1178                database.beginTransaction();
1179                if (dataToDelete.size() > 0) {
1180                    processDataToDelete(database, localeStr, dataToDelete);
1181                }
1182                if (dataToUpdate.size() > 0) {
1183                    processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys,
1184                            forceUpdate);
1185                }
1186                database.setTransactionSuccessful();
1187            } finally {
1188                database.endTransaction();
1189            }
1190
1191            return null;
1192        }
1193
1194        private boolean processDataToUpdate(SQLiteDatabase database, String localeStr,
1195                List<SearchIndexableData> dataToUpdate, Map<String, List<String>> nonIndexableKeys,
1196                boolean forceUpdate) {
1197
1198            if (!forceUpdate && isLocaleAlreadyIndexed(database, localeStr)) {
1199                Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed");
1200                return true;
1201            }
1202
1203            boolean result = false;
1204            final long current = System.currentTimeMillis();
1205
1206            final int count = dataToUpdate.size();
1207            for (int n = 0; n < count; n++) {
1208                final SearchIndexableData data = dataToUpdate.get(n);
1209                try {
1210                    indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys);
1211                } catch (Exception e) {
1212                    Log.e(LOG_TAG,
1213                            "Cannot index: " + data.className + " for locale: " + localeStr, e);
1214                }
1215            }
1216
1217            final long now = System.currentTimeMillis();
1218            Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " +
1219                    (now - current) + " millis");
1220            return result;
1221        }
1222
1223        private boolean processDataToDelete(SQLiteDatabase database, String localeStr,
1224                List<SearchIndexableData> dataToDelete) {
1225
1226            boolean result = false;
1227            final long current = System.currentTimeMillis();
1228
1229            final int count = dataToDelete.size();
1230            for (int n = 0; n < count; n++) {
1231                final SearchIndexableData data = dataToDelete.get(n);
1232                if (data == null) {
1233                    continue;
1234                }
1235                if (!TextUtils.isEmpty(data.className)) {
1236                    delete(database, IndexColumns.CLASS_NAME, data.className);
1237                } else  {
1238                    if (data instanceof SearchIndexableRaw) {
1239                        final SearchIndexableRaw raw = (SearchIndexableRaw) data;
1240                        if (!TextUtils.isEmpty(raw.title)) {
1241                            delete(database, IndexColumns.DATA_TITLE, raw.title);
1242                        }
1243                    }
1244                }
1245            }
1246
1247            final long now = System.currentTimeMillis();
1248            Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " +
1249                    (now - current) + " millis");
1250            return result;
1251        }
1252
1253        private int delete(SQLiteDatabase database, String columName, String value) {
1254            final String whereClause = columName + "=?";
1255            final String[] whereArgs = new String[] { value };
1256
1257            return database.delete(Tables.TABLE_PREFS_INDEX, whereClause, whereArgs);
1258        }
1259
1260        private boolean isLocaleAlreadyIndexed(SQLiteDatabase database, String locale) {
1261            Cursor cursor = null;
1262            boolean result = false;
1263            final StringBuilder sb = new StringBuilder(IndexColumns.LOCALE);
1264            sb.append(" = ");
1265            DatabaseUtils.appendEscapedSQLString(sb, locale);
1266            try {
1267                // We care only for 1 row
1268                cursor = database.query(Tables.TABLE_PREFS_INDEX, null,
1269                        sb.toString(), null, null, null, null, "1");
1270                final int count = cursor.getCount();
1271                result = (count >= 1);
1272            } finally {
1273                if (cursor != null) {
1274                    cursor.close();
1275                }
1276            }
1277            return result;
1278        }
1279    }
1280
1281    /**
1282     * A basic AsyncTask for saving a Search query into the database
1283     */
1284    private class SaveSearchQueryTask extends AsyncTask<String, Void, Long> {
1285
1286        @Override
1287        protected Long doInBackground(String... params) {
1288            final long now = new Date().getTime();
1289
1290            final ContentValues values = new ContentValues();
1291            values.put(IndexDatabaseHelper.SavedQueriesColums.QUERY, params[0]);
1292            values.put(IndexDatabaseHelper.SavedQueriesColums.TIME_STAMP, now);
1293
1294            final SQLiteDatabase database = getWritableDatabase();
1295
1296            long lastInsertedRowId = -1;
1297            try {
1298                // First, delete all saved queries that are the same
1299                database.delete(Tables.TABLE_SAVED_QUERIES,
1300                        IndexDatabaseHelper.SavedQueriesColums.QUERY + " = ?",
1301                        new String[] { params[0] });
1302
1303                // Second, insert the saved query
1304                lastInsertedRowId =
1305                        database.insertOrThrow(Tables.TABLE_SAVED_QUERIES, null, values);
1306
1307                // Last, remove "old" saved queries
1308                final long delta = lastInsertedRowId - MAX_SAVED_SEARCH_QUERY;
1309                if (delta > 0) {
1310                    int count = database.delete(Tables.TABLE_SAVED_QUERIES, "rowId <= ?",
1311                            new String[] { Long.toString(delta) });
1312                    Log.d(LOG_TAG, "Deleted '" + count + "' saved Search query(ies)");
1313                }
1314            } catch (Exception e) {
1315                Log.d(LOG_TAG, "Cannot update saved Search queries", e);
1316            }
1317
1318            return lastInsertedRowId;
1319        }
1320    }
1321}
1322