Home | History | Annotate | Download | only in indexing
      1 /*
      2  * Copyright (C) 2017 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 
     18 package com.android.settings.search.indexing;
     19 
     20 import android.Manifest;
     21 import android.content.ContentResolver;
     22 import android.content.Context;
     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.database.Cursor;
     28 import android.net.Uri;
     29 import android.provider.SearchIndexableResource;
     30 import android.provider.SearchIndexablesContract;
     31 import android.text.TextUtils;
     32 import android.util.ArraySet;
     33 import android.util.Log;
     34 
     35 import com.android.internal.annotations.VisibleForTesting;
     36 import com.android.settings.search.SearchIndexableRaw;
     37 import com.android.settings.search.SettingsSearchIndexablesProvider;
     38 
     39 import java.util.ArrayList;
     40 import java.util.Collections;
     41 import java.util.List;
     42 
     43 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME;
     44 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID;
     45 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID;
     46 
     47 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE;
     48 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME;
     49 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES;
     50 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID;
     51 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION;
     52 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS;
     53 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE;
     54 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY;
     55 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS;
     56 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK;
     57 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE;
     58 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF;
     59 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON;
     60 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE;
     61 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID;
     62 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION;
     63 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS;
     64 import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE;
     65 
     66 /**
     67  * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed.
     68  */
     69 public class PreIndexDataCollector {
     70 
     71     private static final String TAG = "IndexableDataCollector";
     72 
     73     // TODO (b/64938328) update to new search package.
     74     private final String BASE_AUTHORITY = "com.android.settings";
     75 
     76     private static final List<String> EMPTY_LIST = Collections.emptyList();
     77 
     78     private Context mContext;
     79 
     80     private PreIndexData mIndexData;
     81 
     82     public PreIndexDataCollector(Context context) {
     83         mContext = context;
     84     }
     85 
     86     public PreIndexData collectIndexableData(List<ResolveInfo> providers, boolean isFullIndex) {
     87         mIndexData = new PreIndexData();
     88 
     89         for (final ResolveInfo info : providers) {
     90             if (!isWellKnownProvider(info)) {
     91                 continue;
     92             }
     93             final String authority = info.providerInfo.authority;
     94             final String packageName = info.providerInfo.packageName;
     95 
     96             if (isFullIndex) {
     97                 addIndexablesFromRemoteProvider(packageName, authority);
     98             }
     99 
    100             final long nonIndexableStartTime = System.currentTimeMillis();
    101             addNonIndexablesKeysFromRemoteProvider(packageName, authority);
    102             if (SettingsSearchIndexablesProvider.DEBUG) {
    103                 final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime;
    104                 Log.d(TAG, "performIndexing update non-indexable for package " + packageName
    105                         + " took time: " + nonIndexableTime);
    106             }
    107         }
    108 
    109         return mIndexData;
    110     }
    111 
    112     private boolean addIndexablesFromRemoteProvider(String packageName, String authority) {
    113         try {
    114             final Context context = BASE_AUTHORITY.equals(authority) ?
    115                     mContext : mContext.createPackageContext(packageName, 0);
    116 
    117             final Uri uriForResources = buildUriForXmlResources(authority);
    118             mIndexData.dataToUpdate.addAll(getIndexablesForXmlResourceUri(context, packageName,
    119                     uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS));
    120 
    121             final Uri uriForRawData = buildUriForRawData(authority);
    122             mIndexData.dataToUpdate.addAll(getIndexablesForRawDataUri(context, packageName,
    123                     uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS));
    124             return true;
    125         } catch (PackageManager.NameNotFoundException e) {
    126             Log.w(TAG, "Could not create context for " + packageName + ": "
    127                     + Log.getStackTraceString(e));
    128             return false;
    129         }
    130     }
    131 
    132     @VisibleForTesting
    133     List<SearchIndexableResource> getIndexablesForXmlResourceUri(Context packageContext,
    134             String packageName, Uri uri, String[] projection) {
    135 
    136         final ContentResolver resolver = packageContext.getContentResolver();
    137         final Cursor cursor = resolver.query(uri, projection, null, null, null);
    138         List<SearchIndexableResource> resources = new ArrayList<>();
    139 
    140         if (cursor == null) {
    141             Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
    142             return resources;
    143         }
    144 
    145         try {
    146             final int count = cursor.getCount();
    147             if (count > 0) {
    148                 while (cursor.moveToNext()) {
    149                     final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID);
    150 
    151                     final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME);
    152                     final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID);
    153 
    154                     final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION);
    155                     final String targetPackage = cursor.getString(
    156                             COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE);
    157                     final String targetClass = cursor.getString(
    158                             COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS);
    159 
    160                     SearchIndexableResource sir = new SearchIndexableResource(packageContext);
    161                     sir.xmlResId = xmlResId;
    162                     sir.className = className;
    163                     sir.packageName = packageName;
    164                     sir.iconResId = iconResId;
    165                     sir.intentAction = action;
    166                     sir.intentTargetPackage = targetPackage;
    167                     sir.intentTargetClass = targetClass;
    168 
    169                     resources.add(sir);
    170                 }
    171             }
    172         } finally {
    173             cursor.close();
    174         }
    175         return resources;
    176     }
    177 
    178     private void addNonIndexablesKeysFromRemoteProvider(String packageName,
    179             String authority) {
    180         final List<String> keys =
    181                 getNonIndexablesKeysFromRemoteProvider(packageName, authority);
    182 
    183         if (keys != null && !keys.isEmpty()) {
    184             mIndexData.nonIndexableKeys.put(authority, new ArraySet<>(keys));
    185         }
    186     }
    187 
    188     @VisibleForTesting
    189     List<String> getNonIndexablesKeysFromRemoteProvider(String packageName,
    190             String authority) {
    191         try {
    192             final Context packageContext = mContext.createPackageContext(packageName, 0);
    193 
    194             final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority);
    195             return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys,
    196                     SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS);
    197         } catch (PackageManager.NameNotFoundException e) {
    198             Log.w(TAG, "Could not create context for " + packageName + ": "
    199                     + Log.getStackTraceString(e));
    200             return EMPTY_LIST;
    201         }
    202     }
    203 
    204     @VisibleForTesting
    205     Uri buildUriForXmlResources(String authority) {
    206         return Uri.parse("content://" + authority + "/" +
    207                 SearchIndexablesContract.INDEXABLES_XML_RES_PATH);
    208     }
    209 
    210     @VisibleForTesting
    211     Uri buildUriForRawData(String authority) {
    212         return Uri.parse("content://" + authority + "/" +
    213                 SearchIndexablesContract.INDEXABLES_RAW_PATH);
    214     }
    215 
    216     @VisibleForTesting
    217     Uri buildUriForNonIndexableKeys(String authority) {
    218         return Uri.parse("content://" + authority + "/" +
    219                 SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH);
    220     }
    221 
    222     @VisibleForTesting
    223     List<SearchIndexableRaw> getIndexablesForRawDataUri(Context packageContext, String packageName,
    224             Uri uri, String[] projection) {
    225         final ContentResolver resolver = packageContext.getContentResolver();
    226         final Cursor cursor = resolver.query(uri, projection, null, null, null);
    227         List<SearchIndexableRaw> rawData = new ArrayList<>();
    228 
    229         if (cursor == null) {
    230             Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
    231             return rawData;
    232         }
    233 
    234         try {
    235             final int count = cursor.getCount();
    236             if (count > 0) {
    237                 while (cursor.moveToNext()) {
    238                     final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK);
    239                     // TODO Remove rank
    240                     final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE);
    241                     final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON);
    242                     final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF);
    243                     final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES);
    244                     final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS);
    245 
    246                     final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE);
    247 
    248                     final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME);
    249                     final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID);
    250 
    251                     final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION);
    252                     final String targetPackage = cursor.getString(
    253                             COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE);
    254                     final String targetClass = cursor.getString(
    255                             COLUMN_INDEX_RAW_INTENT_TARGET_CLASS);
    256 
    257                     final String key = cursor.getString(COLUMN_INDEX_RAW_KEY);
    258                     final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID);
    259 
    260                     SearchIndexableRaw data = new SearchIndexableRaw(packageContext);
    261                     data.title = title;
    262                     data.summaryOn = summaryOn;
    263                     data.summaryOff = summaryOff;
    264                     data.entries = entries;
    265                     data.keywords = keywords;
    266                     data.screenTitle = screenTitle;
    267                     data.className = className;
    268                     data.packageName = packageName;
    269                     data.iconResId = iconResId;
    270                     data.intentAction = action;
    271                     data.intentTargetPackage = targetPackage;
    272                     data.intentTargetClass = targetClass;
    273                     data.key = key;
    274                     data.userId = userId;
    275 
    276                     rawData.add(data);
    277                 }
    278             }
    279         } finally {
    280             cursor.close();
    281         }
    282 
    283         return rawData;
    284     }
    285 
    286     private List<String> getNonIndexablesKeys(Context packageContext, Uri uri,
    287             String[] projection) {
    288 
    289         final ContentResolver resolver = packageContext.getContentResolver();
    290         final List<String> result = new ArrayList<>();
    291         Cursor cursor;
    292         try {
    293             cursor = resolver.query(uri, projection, null, null, null);
    294         } catch (NullPointerException e) {
    295             Log.e(TAG, "Exception querying the keys!", e);
    296             return result;
    297         }
    298 
    299         if (cursor == null) {
    300             Log.w(TAG, "Cannot add index data for Uri: " + uri.toString());
    301             return result;
    302         }
    303 
    304         try {
    305             final int count = cursor.getCount();
    306             if (count > 0) {
    307                 while (cursor.moveToNext()) {
    308                     final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE);
    309 
    310                     if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) {
    311                         Log.v(TAG, "Empty non-indexable key from: "
    312                                 + packageContext.getPackageName());
    313                         continue;
    314                     }
    315 
    316                     result.add(key);
    317                 }
    318             }
    319             return result;
    320         } finally {
    321             cursor.close();
    322         }
    323     }
    324 
    325     /**
    326      * Only allow a "well known" SearchIndexablesProvider. The provider should:
    327      *
    328      * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES}
    329      * - be from a privileged package
    330      */
    331     @VisibleForTesting
    332     boolean isWellKnownProvider(ResolveInfo info) {
    333         final String authority = info.providerInfo.authority;
    334         final String packageName = info.providerInfo.applicationInfo.packageName;
    335 
    336         if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) {
    337             return false;
    338         }
    339 
    340         final String readPermission = info.providerInfo.readPermission;
    341         final String writePermission = info.providerInfo.writePermission;
    342 
    343         if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) {
    344             return false;
    345         }
    346 
    347         if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) ||
    348                 !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) {
    349             return false;
    350         }
    351 
    352         return isPrivilegedPackage(packageName, mContext);
    353     }
    354 
    355     /**
    356      * @return true if the {@param packageName} is privileged.
    357      */
    358     private boolean isPrivilegedPackage(String packageName, Context context) {
    359         final PackageManager pm = context.getPackageManager();
    360         try {
    361             PackageInfo packInfo = pm.getPackageInfo(packageName, 0);
    362             return ((packInfo.applicationInfo.privateFlags
    363                     & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0);
    364         } catch (PackageManager.NameNotFoundException e) {
    365             return false;
    366         }
    367     }
    368 }
    369