/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.settings.search.indexing; import android.Manifest; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; import android.provider.SearchIndexableResource; import android.provider.SearchIndexablesContract; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.search.SearchIndexableRaw; import com.android.settings.search.SettingsSearchIndexablesProvider; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; /** * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed. */ public class PreIndexDataCollector { private static final String TAG = "IndexableDataCollector"; // TODO (b/64938328) update to new search package. private final String BASE_AUTHORITY = "com.android.settings"; private static final List EMPTY_LIST = Collections.emptyList(); private Context mContext; private PreIndexData mIndexData; public PreIndexDataCollector(Context context) { mContext = context; } public PreIndexData collectIndexableData(List providers, boolean isFullIndex) { mIndexData = new PreIndexData(); for (final ResolveInfo info : providers) { if (!isWellKnownProvider(info)) { continue; } final String authority = info.providerInfo.authority; final String packageName = info.providerInfo.packageName; if (isFullIndex) { addIndexablesFromRemoteProvider(packageName, authority); } final long nonIndexableStartTime = System.currentTimeMillis(); addNonIndexablesKeysFromRemoteProvider(packageName, authority); if (SettingsSearchIndexablesProvider.DEBUG) { final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime; Log.d(TAG, "performIndexing update non-indexable for package " + packageName + " took time: " + nonIndexableTime); } } return mIndexData; } private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { try { final Context context = BASE_AUTHORITY.equals(authority) ? mContext : mContext.createPackageContext(packageName, 0); final Uri uriForResources = buildUriForXmlResources(authority); mIndexData.dataToUpdate.addAll(getIndexablesForXmlResourceUri(context, packageName, uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS)); final Uri uriForRawData = buildUriForRawData(authority); mIndexData.dataToUpdate.addAll(getIndexablesForRawDataUri(context, packageName, uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS)); return true; } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Could not create context for " + packageName + ": " + Log.getStackTraceString(e)); return false; } } @VisibleForTesting List getIndexablesForXmlResourceUri(Context packageContext, String packageName, Uri uri, String[] projection) { final ContentResolver resolver = packageContext.getContentResolver(); final Cursor cursor = resolver.query(uri, projection, null, null, null); List resources = new ArrayList<>(); if (cursor == null) { Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); return resources; } try { final int count = cursor.getCount(); if (count > 0) { while (cursor.moveToNext()) { final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); final String targetPackage = cursor.getString( COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); final String targetClass = cursor.getString( COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); SearchIndexableResource sir = new SearchIndexableResource(packageContext); sir.xmlResId = xmlResId; sir.className = className; sir.packageName = packageName; sir.iconResId = iconResId; sir.intentAction = action; sir.intentTargetPackage = targetPackage; sir.intentTargetClass = targetClass; resources.add(sir); } } } finally { cursor.close(); } return resources; } private void addNonIndexablesKeysFromRemoteProvider(String packageName, String authority) { final List keys = getNonIndexablesKeysFromRemoteProvider(packageName, authority); if (keys != null && !keys.isEmpty()) { mIndexData.nonIndexableKeys.put(authority, new ArraySet<>(keys)); } } @VisibleForTesting List getNonIndexablesKeysFromRemoteProvider(String packageName, String authority) { try { final Context packageContext = mContext.createPackageContext(packageName, 0); final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Could not create context for " + packageName + ": " + Log.getStackTraceString(e)); return EMPTY_LIST; } } @VisibleForTesting Uri buildUriForXmlResources(String authority) { return Uri.parse("content://" + authority + "/" + SearchIndexablesContract.INDEXABLES_XML_RES_PATH); } @VisibleForTesting Uri buildUriForRawData(String authority) { return Uri.parse("content://" + authority + "/" + SearchIndexablesContract.INDEXABLES_RAW_PATH); } @VisibleForTesting Uri buildUriForNonIndexableKeys(String authority) { return Uri.parse("content://" + authority + "/" + SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); } @VisibleForTesting List getIndexablesForRawDataUri(Context packageContext, String packageName, Uri uri, String[] projection) { final ContentResolver resolver = packageContext.getContentResolver(); final Cursor cursor = resolver.query(uri, projection, null, null, null); List rawData = new ArrayList<>(); if (cursor == null) { Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); return rawData; } try { final int count = cursor.getCount(); if (count > 0) { while (cursor.moveToNext()) { final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); // TODO Remove rank final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); final String targetPackage = cursor.getString( COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); final String targetClass = cursor.getString( COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); SearchIndexableRaw data = new SearchIndexableRaw(packageContext); data.title = title; data.summaryOn = summaryOn; data.summaryOff = summaryOff; data.entries = entries; data.keywords = keywords; data.screenTitle = screenTitle; data.className = className; data.packageName = packageName; data.iconResId = iconResId; data.intentAction = action; data.intentTargetPackage = targetPackage; data.intentTargetClass = targetClass; data.key = key; data.userId = userId; rawData.add(data); } } } finally { cursor.close(); } return rawData; } private List getNonIndexablesKeys(Context packageContext, Uri uri, String[] projection) { final ContentResolver resolver = packageContext.getContentResolver(); final List result = new ArrayList<>(); Cursor cursor; try { cursor = resolver.query(uri, projection, null, null, null); } catch (NullPointerException e) { Log.e(TAG, "Exception querying the keys!", e); return result; } if (cursor == null) { Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); return result; } try { final int count = cursor.getCount(); if (count > 0) { while (cursor.moveToNext()) { final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "Empty non-indexable key from: " + packageContext.getPackageName()); continue; } result.add(key); } } return result; } finally { cursor.close(); } } /** * Only allow a "well known" SearchIndexablesProvider. The provider should: * * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} * - be from a privileged package */ @VisibleForTesting boolean isWellKnownProvider(ResolveInfo info) { final String authority = info.providerInfo.authority; final String packageName = info.providerInfo.applicationInfo.packageName; if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { return false; } final String readPermission = info.providerInfo.readPermission; final String writePermission = info.providerInfo.writePermission; if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { return false; } if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { return false; } return isPrivilegedPackage(packageName, mContext); } /** * @return true if the {@param packageName} is privileged. */ private boolean isPrivilegedPackage(String packageName, Context context) { final PackageManager pm = context.getPackageManager(); try { PackageInfo packInfo = pm.getPackageInfo(packageName, 0); return ((packInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0); } catch (PackageManager.NameNotFoundException e) { return false; } } }