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