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