diff --git a/src/com/android/settings/search/DatabaseIndexingManager.java b/src/com/android/settings/search/DatabaseIndexingManager.java index 7f6f0129548..a0f47d16542 100644 --- a/src/com/android/settings/search/DatabaseIndexingManager.java +++ b/src/com/android/settings/search/DatabaseIndexingManager.java @@ -17,27 +17,6 @@ package com.android.settings.search; -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_CLASS_NAME; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; -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; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID; import static com.android.settings.search.DatabaseResultLoader .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE; @@ -70,17 +49,14 @@ import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.provider.SearchIndexableData; @@ -89,7 +65,6 @@ import android.provider.SearchIndexablesContract; import android.support.annotation.DrawableRes; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; -import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; @@ -98,13 +73,13 @@ import com.android.settings.SettingsActivity; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search.indexing.IndexableDataCollector; +import com.android.settings.search.indexing.PreIndexData; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -132,23 +107,14 @@ public class DatabaseIndexingManager { private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; - private static final List EMPTY_LIST = Collections.emptyList(); - - private final String mBaseAuthority; - @VisibleForTesting final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false); - @VisibleForTesting - final UpdateData mDataToProcess = new UpdateData(); + private IndexableDataCollector mCollector; + private Context mContext; - public DatabaseIndexingManager(Context context, String baseAuthority) { - mContext = context; - mBaseAuthority = baseAuthority; - } - - public void setContext(Context context) { + public DatabaseIndexingManager(Context context) { mContext = context; } @@ -177,33 +143,17 @@ public class DatabaseIndexingManager { final String providerVersionedNames = IndexDatabaseHelper.buildProviderVersionedNames(providers); - final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, localeStr, + final boolean isFullIndex = isFullIndex(mContext, localeStr, fingerprint, providerVersionedNames); if (isFullIndex) { rebuildDatabase(); } - for (final ResolveInfo info : providers) { - if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) { - continue; - } - final String authority = info.providerInfo.authority; - final String packageName = info.providerInfo.packageName; + PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex); - if (isFullIndex) { - addIndexablesFromRemoteProvider(packageName, authority); - } - final long nonIndexableStartTime = System.currentTimeMillis(); - addNonIndexablesKeysFromRemoteProvider(packageName, authority); - if (SettingsSearchIndexablesProvider.DEBUG) { - final long nonIndextableTime = System.currentTimeMillis() - nonIndexableStartTime; - Log.d(LOG_TAG, "performIndexing update non-indexable for package " + packageName - + " took time: " + nonIndextableTime); - } - } final long updateDatabaseStartTime = System.currentTimeMillis(); - updateDatabase(isFullIndex, localeStr); + updateDatabase(indexData, isFullIndex, localeStr); if (SettingsSearchIndexablesProvider.DEBUG) { final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime; Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime); @@ -221,10 +171,37 @@ public class DatabaseIndexingManager { } } + @VisibleForTesting + PreIndexData getIndexDataFromProviders(List providers, boolean isFullIndex) { + if (mCollector == null) { + mCollector = new IndexableDataCollector(mContext); + } + return mCollector.collectIndexableData(providers, isFullIndex); + } + /** - * Reconstruct the database in the following cases: - * - Language has changed - * - Build has changed + * Checks if the indexed data is obsolete, when either: + * - Device language has changed + * - Device has taken an OTA. + * In both cases, the device requires a full index. + * + * @param locale is the default for the device + * @param fingerprint id for the current build. + * @return true if a full index should be preformed. + */ + @VisibleForTesting + boolean isFullIndex(Context context, String locale, String fingerprint, + String providerVersionedNames) { + final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale); + final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint); + final boolean areProvidersIndexed = IndexDatabaseHelper + .areProvidersIndexed(context, providerVersionedNames); + + return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed); + } + + /** + * Drop the currently stored database, and clear the flags which mark the database as indexed. */ private void rebuildDatabase() { // Drop the database when the locale or build has changed. This eliminates rows which are @@ -244,16 +221,9 @@ public class DatabaseIndexingManager { * @param localeStr the default locale for the device. */ @VisibleForTesting - void updateDatabase(boolean needsReindexing, String localeStr) { - final UpdateData copy; - - synchronized (mDataToProcess) { - copy = mDataToProcess.copy(); - mDataToProcess.clear(); - } - - final List dataToUpdate = copy.dataToUpdate; - final Map> nonIndexableKeys = copy.nonIndexableKeys; + void updateDatabase(PreIndexData indexData, boolean needsReindexing, String localeStr) { + final List dataToUpdate = indexData.dataToUpdate; + final Map> nonIndexableKeys = indexData.nonIndexableKeys; final SQLiteDatabase database = getWritableDatabase(); if (database == null) { @@ -378,99 +348,10 @@ public class DatabaseIndexingManager { disabledResults.close(); } - @VisibleForTesting - boolean addIndexablesFromRemoteProvider(String packageName, String authority) { - try { - final Context context = mBaseAuthority.equals(authority) ? - mContext : mContext.createPackageContext(packageName, 0); - - final Uri uriForResources = buildUriForXmlResources(authority); - addIndexablesForXmlResourceUri(context, packageName, uriForResources, - SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS); - - final Uri uriForRawData = buildUriForRawData(authority); - addIndexablesForRawDataUri(context, packageName, uriForRawData, - SearchIndexablesContract.INDEXABLES_RAW_COLUMNS); - return true; - } catch (PackageManager.NameNotFoundException e) { - Log.w(LOG_TAG, "Could not create context for " + packageName + ": " - + Log.getStackTraceString(e)); - return false; - } - } - - @VisibleForTesting - void addNonIndexablesKeysFromRemoteProvider(String packageName, - String authority) { - final List keys = - getNonIndexablesKeysFromRemoteProvider(packageName, authority); - - addNonIndexableKeys(packageName, keys); - } - - private 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(LOG_TAG, "Could not create context for " + packageName + ": " - + Log.getStackTraceString(e)); - return EMPTY_LIST; - } - } - - private List getNonIndexablesKeys(Context packageContext, Uri uri, - String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - - if (cursor == null) { - Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); - return EMPTY_LIST; - } - - final List result = new ArrayList<>(); - 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(LOG_TAG, Log.VERBOSE)) { - Log.v(LOG_TAG, "Empty non-indexable key from: " - + packageContext.getPackageName()); - continue; - } - - result.add(key); - } - } - return result; - } finally { - cursor.close(); - } - } - - public void addIndexableData(SearchIndexableData data) { - synchronized (mDataToProcess) { - mDataToProcess.dataToUpdate.add(data); - } - } - - public void addNonIndexableKeys(String authority, List keys) { - synchronized (mDataToProcess) { - if (keys != null && !keys.isEmpty()) { - mDataToProcess.nonIndexableKeys.put(authority, new ArraySet<>(keys)); - } - } - } /** + * TODO (b/64951285): Deprecate this method + * * Update the Index for a specific class name resources * * @param className the class name (typically a fragment name). @@ -491,9 +372,9 @@ public class DatabaseIndexingManager { AsyncTask.execute(new Runnable() { @Override public void run() { - addIndexableData(res); - updateDatabase(false, Locale.getDefault().toString()); - res.enabled = false; +// addIndexableData(res); +// updateDatabase(false, Locale.getDefault().toString()); +// res.enabled = false; } }); } @@ -507,126 +388,9 @@ public class DatabaseIndexingManager { } } - private static Uri buildUriForXmlResources(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.INDEXABLES_XML_RES_PATH); - } - private static Uri buildUriForRawData(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.INDEXABLES_RAW_PATH); - } - - private static Uri buildUriForNonIndexableKeys(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); - } - - private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, - Uri uri, String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - - if (cursor == null) { - Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); - return; - } - - 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; - - addIndexableData(sir); - } - } - } finally { - cursor.close(); - } - } - - private void addIndexablesForRawDataUri(Context packageContext, String packageName, - Uri uri, String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - - if (cursor == null) { - Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); - return; - } - - 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; - - addIndexableData(data); - } - } - } finally { - cursor.close(); - } - } - - public void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, + @VisibleForTesting + void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, SearchIndexableData data, Map> nonIndexableKeys) { if (data instanceof SearchIndexableResource) { indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); @@ -1010,38 +774,6 @@ public class DatabaseIndexingManager { } } - /** - * A private class to describe the indexDatabase data for the Index database - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - static class UpdateData { - public List dataToUpdate; - public List dataToDisable; - public Map> nonIndexableKeys; - - public UpdateData() { - dataToUpdate = new ArrayList<>(); - dataToDisable = new ArrayList<>(); - nonIndexableKeys = new HashMap<>(); - } - - public UpdateData(UpdateData other) { - dataToUpdate = new ArrayList<>(other.dataToUpdate); - dataToDisable = new ArrayList<>(other.dataToDisable); - nonIndexableKeys = new HashMap<>(other.nonIndexableKeys); - } - - public UpdateData copy() { - return new UpdateData(this); - } - - public void clear() { - dataToUpdate.clear(); - dataToDisable.clear(); - nonIndexableKeys.clear(); - } - } - public static class DatabaseRow { public final String locale; public final String updatedTitle; diff --git a/src/com/android/settings/search/DatabaseIndexingUtils.java b/src/com/android/settings/search/DatabaseIndexingUtils.java index 938ddb74b3f..7093134d769 100644 --- a/src/com/android/settings/search/DatabaseIndexingUtils.java +++ b/src/com/android/settings/search/DatabaseIndexingUtils.java @@ -174,35 +174,6 @@ public class DatabaseIndexingUtils { return null; } - /** - * Only allow a "well known" SearchIndexablesProvider. The provider should: - * - * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} - * - be from a privileged package - */ - static boolean isWellKnownProvider(ResolveInfo info, Context context) { - 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, context); - } - static String normalizeHyphen(String input) { return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; } @@ -217,15 +188,4 @@ public class DatabaseIndexingUtils { static String normalizeKeywords(String input) { return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; } - - private static 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; - } - } } diff --git a/src/com/android/settings/search/DatabaseResultLoader.java b/src/com/android/settings/search/DatabaseResultLoader.java index 7815a45ad2c..3d280b2002e 100644 --- a/src/com/android/settings/search/DatabaseResultLoader.java +++ b/src/com/android/settings/search/DatabaseResultLoader.java @@ -40,19 +40,19 @@ public class DatabaseResultLoader extends AsyncLoader providers) { StringBuilder sb = new StringBuilder(); @@ -282,44 +268,42 @@ public class IndexDatabaseHelper extends SQLiteOpenHelper { return sb.toString(); } - static void clearCachedIndexed(Context context) { - context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).edit().clear().commit(); - } - static void setLocaleIndexed(Context context, String locale) { - context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) + context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() .putBoolean(locale, true) .apply(); } static void setProvidersIndexed(Context context, String providerVersionedNames) { - context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) + context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames) .apply(); } static boolean isLocaleAlreadyIndexed(Context context, String locale) { - return context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).getBoolean(locale, false); + return context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) + .getBoolean(locale, false); } static boolean areProvidersIndexed(Context context, String providerVersionedNames) { - final String indexedProviders = context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) - .getString(PREF_KEY_INDEXED_PROVIDERS, null); + final String indexedProviders = + context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) + .getString(PREF_KEY_INDEXED_PROVIDERS, null); return TextUtils.equals(indexedProviders, providerVersionedNames); } static boolean isBuildIndexed(Context context, String buildNo) { - return context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).getBoolean(buildNo, false); + return context.getSharedPreferences(SHARED_PREFS_TAG, + Context.MODE_PRIVATE).getBoolean(buildNo, false); } static void setBuildIndexed(Context context, String buildNo) { - context.getSharedPreferences(INDEX, 0).edit().putBoolean(buildNo, true).commit(); + context.getSharedPreferences(SHARED_PREFS_TAG, 0).edit().putBoolean(buildNo, true).commit(); } private void dropTables(SQLiteDatabase db) { - clearCachedIndexed(mContext); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); diff --git a/src/com/android/settings/search/SearchFeatureProviderImpl.java b/src/com/android/settings/search/SearchFeatureProviderImpl.java index 400cf8fe5f3..69c086f1659 100644 --- a/src/com/android/settings/search/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search/SearchFeatureProviderImpl.java @@ -74,8 +74,7 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { @Override public DatabaseIndexingManager getIndexingManager(Context context) { if (mDatabaseIndexingManager == null) { - mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext(), - context.getPackageName()); + mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext()); } return mDatabaseIndexingManager; } diff --git a/src/com/android/settings/search/indexing/IndexableDataCollector.java b/src/com/android/settings/search/indexing/IndexableDataCollector.java new file mode 100644 index 00000000000..cd6b9bbd2c0 --- /dev/null +++ b/src/com/android/settings/search/indexing/IndexableDataCollector.java @@ -0,0 +1,361 @@ +/* + * 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 IndexableDataCollector { + + 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 IndexableDataCollector(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 Cursor cursor = resolver.query(uri, projection, null, null, null); + final List result = new ArrayList<>(); + + 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; + } + } +} diff --git a/src/com/android/settings/search/indexing/PreIndexData.java b/src/com/android/settings/search/indexing/PreIndexData.java new file mode 100644 index 00000000000..de3cf7ca273 --- /dev/null +++ b/src/com/android/settings/search/indexing/PreIndexData.java @@ -0,0 +1,55 @@ +/* + * 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.provider.SearchIndexableData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * Holds Data sources for indexable data. + * TODO (b/33577327) add getters and setters for data. + */ +public class PreIndexData { + public List dataToUpdate; + public Map> nonIndexableKeys; + + public PreIndexData() { + dataToUpdate = new ArrayList<>(); + nonIndexableKeys = new HashMap<>(); + } + + public PreIndexData(PreIndexData other) { + dataToUpdate = new ArrayList<>(other.dataToUpdate); + nonIndexableKeys = new HashMap<>(other.nonIndexableKeys); + } + + public PreIndexData copy() { + return new PreIndexData(this); + } + + public void clear() { + dataToUpdate.clear(); + nonIndexableKeys.clear(); + } +} diff --git a/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java b/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java index 70ed568447d..fff38c59322 100644 --- a/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java +++ b/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java @@ -17,24 +17,20 @@ package com.android.settings.search; -import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -43,19 +39,18 @@ import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.database.Cursor; -import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; import android.os.Build; +import android.provider.SearchIndexableData; import android.provider.SearchIndexableResource; import android.util.ArrayMap; import com.android.settings.R; import com.android.settings.TestConfig; +import com.android.settings.search.indexing.PreIndexData; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; -import com.android.settings.testutils.shadow.ShadowDatabaseIndexingUtils; import com.android.settings.testutils.shadow.ShadowRunnableAsyncTask; import org.junit.After; @@ -67,7 +62,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowContentResolver; import java.util.ArrayList; import java.util.Arrays; @@ -84,8 +78,6 @@ import java.util.Set; sdk = TestConfig.SDK_VERSION, shadows = { ShadowRunnableAsyncTask.class, - ShadowDatabaseIndexingUtils.class, - ShadowContentResolver.class } ) public class DatabaseIndexingManagerTest { @@ -129,6 +121,8 @@ public class DatabaseIndexingManagerTest { private DatabaseIndexingManager mManager; private SQLiteDatabase mDb; + private final List FAKE_PROVIDER_LIST = new ArrayList<>(); + @Mock private PackageManager mPackageManager; @@ -136,10 +130,12 @@ public class DatabaseIndexingManagerTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - mManager = spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); + mManager = spy(new DatabaseIndexingManager(mContext)); mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn(FAKE_PROVIDER_LIST).when(mPackageManager) + .queryIntentContentProviders(any(Intent.class), anyInt()); FakeFeatureFactory.setupForTest(mContext); } @@ -755,113 +751,60 @@ public class DatabaseIndexingManagerTest { @Test public void testPerformIndexing_fullIndex_getsDataFromProviders() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider(AUTHORITY_ONE, provider); + SearchIndexableRaw rawData = getFakeRaw(); + PreIndexData data = getPreIndexData(rawData); + doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); + doReturn(true).when(mManager).isFullIndex(any(Context.class), anyString(), anyString(), + anyString()); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); + mManager.performIndexing(); - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.performIndexing(); - - verify(manager).addIndexablesFromRemoteProvider(PACKAGE_ONE, AUTHORITY_ONE); - verify(manager).updateDatabase(true /* isFullIndex */, Locale.getDefault().toString()); + verify(mManager).updateDatabase(data, true /* isFullIndex */, + Locale.getDefault().toString()); } @Test - public void testPerformIndexing_incrementalIndex_noDataAdded() { - final List providerInfo = getDummyResolveInfo(); - skipFullIndex(providerInfo); - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider(AUTHORITY_ONE, provider); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(providerInfo); - + public void testPerformIndexing_fullIndex_databaseDropped() { + // Initialize the Manager and force rebuild DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.mDataToProcess.dataToUpdate.clear(); - - manager.performIndexing(); - - verify(manager, times(0)).addDataToDatabase(any(SQLiteDatabase.class), anyString(), - anyList(), anyMap()); - verify(manager, times(0)).addIndexablesFromRemoteProvider(PACKAGE_ONE, AUTHORITY_ONE); - verify(manager).updateDataInDatabase(any(SQLiteDatabase.class), anyMap()); - } - - @Test - public void testPerformIndexing_localeChanged_databaseDropped() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider(AUTHORITY_ONE, provider); - - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); - - // Initialize the Manager - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); + spy(new DatabaseIndexingManager(mContext)); + doReturn(false).when(mManager).isFullIndex(any(Context.class), anyString(), anyString(), + anyString()); // Insert data point which will be dropped - final String oldTitle = "This is French"; - insertSpecialCase(oldTitle, true, "key"); - - // Add a data point to be added by the indexing - SearchIndexableRaw raw = new SearchIndexableRaw(mContext); - final String newTitle = "This is English"; - raw.title = newTitle; - manager.mDataToProcess.dataToUpdate.add(raw); + insertSpecialCase("Ceci n'est pas un pipe", true, "oui oui mon ami"); manager.performIndexing(); - // Assert that the New Title is inserted - final Cursor newCursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE data_title = '" + - newTitle + "'", null); - assertThat(newCursor.getCount()).isEqualTo(1); - // Assert that the Old Title is no longer in the database, since it was dropped - final Cursor oldCursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE data_title = '" + - oldTitle + "'", null); + final Cursor oldCursor = mDb.rawQuery("SELECT * FROM prefs_index", null); + assertThat(oldCursor.getCount()).isEqualTo(0); } @Test - public void testPerformIndexing_onOta_FullIndex() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider( - AUTHORITY_ONE, provider - ); + public void testPerformIndexing_isfullIndex() { + SearchIndexableRaw rawData = getFakeRaw(); + PreIndexData data = getPreIndexData(rawData); + doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); + doReturn(true).when(mManager).isFullIndex(any(Context.class), anyString(), anyString(), + anyString()); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); + mManager.performIndexing(); - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.performIndexing(); - - verify(manager).updateDatabase(true /* isFullIndex */, Locale.getDefault().toString()); + verify(mManager).updateDatabase(data, true /* isFullIndex */, + Locale.getDefault().toString()); } @Test - public void testPerformIndexing_onPackageChange_shouldFullIndex() { + public void testPerformIndexing_onPackageChange_fullIndex() { final List providers = getDummyResolveInfo(); final String buildNumber = Build.FINGERPRINT; final String locale = Locale.getDefault().toString(); skipFullIndex(providers); // This snapshot is already indexed. Should return false - assertThat(IndexDatabaseHelper.isFullIndex( + assertThat(mManager.isFullIndex( mContext, locale, buildNumber, IndexDatabaseHelper.buildProviderVersionedNames(providers))) .isFalse(); @@ -869,65 +812,46 @@ public class DatabaseIndexingManagerTest { // Change provider version number, this should trigger full index. providers.get(0).providerInfo.applicationInfo.versionCode++; - assertThat(IndexDatabaseHelper.isFullIndex(mContext, locale, buildNumber, + assertThat(mManager.isFullIndex(mContext, locale, buildNumber, IndexDatabaseHelper.buildProviderVersionedNames(providers))) .isTrue(); } @Test public void testPerformIndexing_onOta_buildNumberIsCached() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider( - AUTHORITY_ONE, provider - ); + mManager.performIndexing(); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); - - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.performIndexing(); - - assertThat(IndexDatabaseHelper.getInstance(mContext).isBuildIndexed(mContext, - Build.FINGERPRINT)).isTrue(); + assertThat(IndexDatabaseHelper.isBuildIndexed(mContext, Build.FINGERPRINT)).isTrue(); } @Test public void testFullUpdatedDatabase_noData_addDataToDatabaseNotCalled() { - mManager.updateDatabase(true /* isFullIndex */, localeStr); - mManager.mDataToProcess.dataToUpdate.clear(); + PreIndexData emptydata = new PreIndexData(); + mManager.updateDatabase(emptydata, true /* isFullIndex */, localeStr); verify(mManager, times(0)).addDataToDatabase(any(SQLiteDatabase.class), anyString(), anyList(), anyMap()); } - @Test - public void testFullUpdatedDatabase_updatedDataInDatabaseNotCalled() { - mManager.updateDatabase(true /* isFullIndex */, localeStr); - verify(mManager, times(0)).updateDataInDatabase(any(SQLiteDatabase.class), anyMap()); - } - @Test public void testLocaleUpdated_afterIndexing_localeNotAdded() { - mManager.updateDatabase(true /* isFullIndex */, localeStr); - assertThat(IndexDatabaseHelper.getInstance(mContext) - .isLocaleAlreadyIndexed(mContext, localeStr)).isFalse(); + PreIndexData emptydata = new PreIndexData(); + mManager.updateDatabase(emptydata, true /* isFullIndex */, localeStr); + + assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isFalse(); } @Test public void testLocaleUpdated_afterFullIndexing_localeAdded() { mManager.performIndexing(); - assertThat(IndexDatabaseHelper.getInstance(mContext) - .isLocaleAlreadyIndexed(mContext, localeStr)).isTrue(); + assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isTrue(); } @Test public void testUpdateDatabase_newEligibleData_addedToDatabase() { // Test that addDataToDatabase is called when dataToUpdate is non-empty - mManager.mDataToProcess.dataToUpdate.add(getFakeRaw()); - mManager.updateDatabase(true /* isFullIndex */, localeStr); + PreIndexData indexData = new PreIndexData(); + indexData.dataToUpdate.add(getFakeRaw()); + mManager.updateDatabase(indexData, true /* isFullIndex */, localeStr); Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index", null); cursor.moveToPosition(0); @@ -1020,8 +944,8 @@ public class DatabaseIndexingManagerTest { @Test public void testEmptyNonIndexableKeys_emptyDataKeyResources_addedToDatabase() { insertSpecialCase(TITLE_ONE, true /* enabled */, null /* dataReferenceKey */); - - mManager.updateDatabase(false, localeStr); + PreIndexData emptydata = new PreIndexData(); + mManager.updateDatabase(emptydata, false /* needsReindexing */, localeStr); Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 1", null); cursor.moveToPosition(0); @@ -1111,46 +1035,6 @@ public class DatabaseIndexingManagerTest { return niks; } - private List getDummyResolveInfo() { - List infoList = new ArrayList<>(); - ResolveInfo info = new ResolveInfo(); - info.providerInfo = new ProviderInfo(); - info.providerInfo.exported = true; - info.providerInfo.authority = AUTHORITY_ONE; - info.providerInfo.packageName = PACKAGE_ONE; - info.providerInfo.applicationInfo = new ApplicationInfo(); - infoList.add(info); - - return infoList; - } - - // TODO move this method and its counterpart in CursorToSearchResultConverterTest into - // a util class with public fields to assert values. - private Cursor getDummyCursor() { - MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); - final String BLANK = ""; - - ArrayList item = - new ArrayList<>(INDEXABLES_RAW_COLUMNS.length); - item.add("42"); // Rank - item.add(TITLE_ONE); // Title - item.add(BLANK); // Summary on - item.add(BLANK); // summary off - item.add(BLANK); // entries - item.add(BLANK); // keywords - item.add(BLANK); // screen title - item.add(BLANK); // classname - item.add("123"); // Icon - item.add(BLANK); // Intent action - item.add(BLANK); // target package - item.add(BLANK); // target class - item.add(KEY_ONE); // Key - item.add("-1"); // userId - cursor.addRow(item); - - return cursor; - } - private void insertSpecialCase(String specialCase, boolean enabled, String key) { ContentValues values = new ContentValues(); values.put(IndexDatabaseHelper.IndexColumns.DOCID, specialCase.hashCode()); @@ -1179,43 +1063,22 @@ public class DatabaseIndexingManagerTest { mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); } - private class DummyProvider extends ContentProvider { + private PreIndexData getPreIndexData(SearchIndexableData fakeData) { + PreIndexData data = new PreIndexData(); + data.dataToUpdate.add(fakeData); + return data; + } - @Override - public boolean onCreate() { - return false; - } + private List getDummyResolveInfo() { + List infoList = new ArrayList<>(); + ResolveInfo info = new ResolveInfo(); + info.providerInfo = new ProviderInfo(); + info.providerInfo.exported = true; + info.providerInfo.authority = AUTHORITY_ONE; + info.providerInfo.packageName = PACKAGE_ONE; + info.providerInfo.applicationInfo = new ApplicationInfo(); + infoList.add(info); - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, - @Nullable String selection, @Nullable String[] selectionArgs, - @Nullable String sortOrder) { - if (uri.toString().contains("xml")) { - return null; - } - return getDummyCursor(); - } - - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - return null; - } - - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, - @Nullable String[] selectionArgs) { - return 0; - } - - @Override - public int update(@NonNull Uri uri, @Nullable ContentValues values, - @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } + return infoList; } } diff --git a/tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java b/tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java new file mode 100644 index 00000000000..0f1f34524d4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java @@ -0,0 +1,170 @@ +/* + * 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.content.ContentResolver; +import android.content.Context; + +import android.content.pm.ApplicationInfo; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.provider.SearchIndexableResource; +import com.android.settings.TestConfig; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class IndexableDataCollectorTest { + + private final String AUTHORITY_ONE = "authority"; + private final String PACKAGE_ONE = "com.android.settings"; + + @Mock + ContentResolver mResolver; + + Context mContext; + + IndexableDataCollector mDataCollector; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + doReturn(mResolver).when(mContext).getContentResolver(); + //doReturn(mPackageManager).when(mContext).getPackageManager(); + + mDataCollector = spy(new IndexableDataCollector(mContext)); + } + + @Test + public void testCollectIndexableData_addsResourceData() { + final List providerInfo = getDummyResolveInfo(); + doReturn(true).when(mDataCollector).isWellKnownProvider(any(ResolveInfo.class)); + + List resources = getFakeResource(); + doReturn(resources).when(mDataCollector).getIndexablesForXmlResourceUri( + any(Context.class), anyString(), any(Uri.class), any(String[].class)); + + PreIndexData data = mDataCollector.collectIndexableData(providerInfo, + true /* isFullIndex */); + + assertThat(data.dataToUpdate).containsAllIn(resources); + } + + @Test + public void testCollectIndexableData_addsRawData() { + final List providerInfo = getDummyResolveInfo(); + doReturn(true).when(mDataCollector).isWellKnownProvider(any(ResolveInfo.class)); + + List rawData = getFakeRaw(); + doReturn(rawData).when(mDataCollector).getIndexablesForRawDataUri(any(Context.class), + anyString(), any(Uri.class), any(String[].class)); + + + PreIndexData data = mDataCollector.collectIndexableData(providerInfo, + true /* isFullIndex */); + + assertThat(data.dataToUpdate).containsAllIn(rawData); + } + + @Test + public void testCollectIndexableData_addsNonIndexables() { + final List providerInfo = getDummyResolveInfo(); + doReturn(true).when(mDataCollector).isWellKnownProvider(any(ResolveInfo.class)); + + List niks = getFakeNonIndexables(); + + doReturn(niks).when(mDataCollector).getNonIndexablesKeysFromRemoteProvider(anyString(), + anyString()); + + PreIndexData data = mDataCollector.collectIndexableData(providerInfo, + true /* isFullIndex */); + + assertThat(data.nonIndexableKeys.get(AUTHORITY_ONE)).containsAllIn(niks); + } + + private List getDummyResolveInfo() { + List infoList = new ArrayList<>(); + ResolveInfo info = new ResolveInfo(); + info.providerInfo = new ProviderInfo(); + info.providerInfo.exported = true; + info.providerInfo.authority = AUTHORITY_ONE; + info.providerInfo.packageName = PACKAGE_ONE; + info.providerInfo.applicationInfo = new ApplicationInfo(); + infoList.add(info); + + return infoList; + } + + private List getFakeResource() { + List resources = new ArrayList<>(); + final String BLANK = ""; + + SearchIndexableResource sir = new SearchIndexableResource(mContext); + sir.rank = 0; + sir.xmlResId = 0; + sir.className = BLANK; + sir.packageName = BLANK; + sir.iconResId = 0; + sir.intentAction = BLANK; + sir.intentTargetPackage = BLANK; + sir.intentTargetClass = BLANK; + sir.enabled = true; + resources.add(sir); + + return resources; + } + + private List getFakeRaw() { + List rawData = new ArrayList<>(); + + SearchIndexableRaw data = new SearchIndexableRaw(mContext); + data.title = "bront"; + data.key = "brint"; + rawData.add(data); + + return rawData; + } + + private List getFakeNonIndexables() { + List niks = new ArrayList<>(); + niks.add("they're"); + niks.add("good"); + niks.add("dogs"); + niks.add("brent"); + return niks; + } +}