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