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