diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index fe25a8f9265..aa765171ad4 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -253,7 +253,7 @@ public class SettingsActivity extends SettingsDrawerActivity String action = intent.getAction(); if (action.equals(Intent.ACTION_USER_ADDED) || action.equals(Intent.ACTION_USER_REMOVED)) { - Index.getInstance(getApplicationContext()).update(); + mSearchFeatureProvider.updateIndex(getApplicationContext()); } } }; @@ -310,7 +310,7 @@ public class SettingsActivity extends SettingsDrawerActivity @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - Index.getInstance(this).update(); + mSearchFeatureProvider.updateIndex(getApplicationContext()); } @Override diff --git a/src/com/android/settings/search2/DatabaseIndexingManager.java b/src/com/android/settings/search2/DatabaseIndexingManager.java new file mode 100644 index 00000000000..9bb4538aa2c --- /dev/null +++ b/src/com/android/settings/search2/DatabaseIndexingManager.java @@ -0,0 +1,963 @@ +/* + * Copyright (C) 2016 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.search2; + +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.database.sqlite.SQLiteFullException; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.SearchIndexableData; +import android.provider.SearchIndexableResource; +import android.provider.SearchIndexablesContract; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import com.android.settings.search.IndexDatabaseHelper; +import com.android.settings.search.Indexable; +import com.android.settings.search.Ranking; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.search.SearchIndexableResources; + +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; +import java.util.concurrent.atomic.AtomicBoolean; + +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_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; + +/** + * Consumes the SearchIndexableProvider content providers. + * Updates the Resource, Raw Data and non-indexable data for Search. + */ +public class DatabaseIndexingManager { + private static final String LOG_TAG = "DatabaseIndexingManager"; + + // Those indices should match the indices of SELECT_COLUMNS ! + public static final int COLUMN_INDEX_RANK = 0; + public static final int COLUMN_INDEX_TITLE = 1; + public static final int COLUMN_INDEX_SUMMARY_ON = 2; + public static final int COLUMN_INDEX_SUMMARY_OFF = 3; + public static final int COLUMN_INDEX_ENTRIES = 4; + public static final int COLUMN_INDEX_KEYWORDS = 5; + public static final int COLUMN_INDEX_CLASS_NAME = 6; + public static final int COLUMN_INDEX_SCREEN_TITLE = 7; + public static final int COLUMN_INDEX_ICON = 8; + public static final int COLUMN_INDEX_INTENT_ACTION = 9; + public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 10; + public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 11; + public static final int COLUMN_INDEX_ENABLED = 12; + public static final int COLUMN_INDEX_KEY = 13; + public static final int COLUMN_INDEX_USER_ID = 14; + + // If you change the order of columns here, you SHOULD change the COLUMN_INDEX_XXX values + private static final String[] SELECT_COLUMNS = new String[] { + IndexDatabaseHelper.IndexColumns.DATA_RANK, // 0 + IndexDatabaseHelper.IndexColumns.DATA_TITLE, // 1 + IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON, // 2 + IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF, // 3 + IndexDatabaseHelper.IndexColumns.DATA_ENTRIES, // 4 + IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS, // 5 + IndexDatabaseHelper.IndexColumns.CLASS_NAME, // 6 + IndexDatabaseHelper.IndexColumns.SCREEN_TITLE, // 7 + IndexDatabaseHelper.IndexColumns.ICON, // 8 + IndexDatabaseHelper.IndexColumns.INTENT_ACTION, // 9 + IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE, // 10 + IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS, // 11 + IndexDatabaseHelper.IndexColumns.ENABLED, // 12 + IndexDatabaseHelper.IndexColumns.DATA_KEY_REF // 13 + }; + + private static final String[] MATCH_COLUMNS_PRIMARY = { + IndexDatabaseHelper.IndexColumns.DATA_TITLE, + IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED, + IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS + }; + + private static final String[] MATCH_COLUMNS_SECONDARY = { + IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON, + IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED, + IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF, + IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, + IndexDatabaseHelper.IndexColumns.DATA_ENTRIES + }; + + + private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen"; + 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; + + /** + * A private class to describe the update data for the Index database + */ + private static class UpdateData { + public List dataToUpdate; + public List dataToDelete; + public Map> nonIndexableKeys; + + public boolean forceUpdate; + public boolean fullIndex; + + public UpdateData() { + dataToUpdate = new ArrayList(); + dataToDelete = new ArrayList(); + nonIndexableKeys = new HashMap>(); + } + + public UpdateData(DatabaseIndexingManager.UpdateData other) { + dataToUpdate = new ArrayList(other.dataToUpdate); + dataToDelete = new ArrayList(other.dataToDelete); + nonIndexableKeys = new HashMap>(other.nonIndexableKeys); + forceUpdate = other.forceUpdate; + fullIndex = other.fullIndex; + } + + public DatabaseIndexingManager.UpdateData copy() { + return new DatabaseIndexingManager.UpdateData(this); + } + + public void clear() { + dataToUpdate.clear(); + dataToDelete.clear(); + nonIndexableKeys.clear(); + forceUpdate = false; + fullIndex = false; + } + } + + private final AtomicBoolean mIsAvailable = new AtomicBoolean(false); + private final DatabaseIndexingManager.UpdateData mDataToProcess = + new DatabaseIndexingManager.UpdateData(); + private Context mContext; + + public DatabaseIndexingManager(Context context, String baseAuthority) { + mContext = context; + mBaseAuthority = baseAuthority; + } + + public void setContext(Context context) { + mContext = context; + } + + public boolean isAvailable() { + return mIsAvailable.get(); + } + + public void update() { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + final Intent intent = new Intent(SearchIndexablesContract.PROVIDER_INTERFACE); + List list = + mContext.getPackageManager().queryIntentContentProviders(intent, 0); + + final int size = list.size(); + for (int n = 0; n < size; n++) { + final ResolveInfo info = list.get(n); + if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) { + continue; + } + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.packageName; + + addIndexablesFromRemoteProvider(packageName, authority); + addNonIndexablesKeysFromRemoteProvider(packageName, authority); + } + + mDataToProcess.fullIndex = true; + updateInternal(); + } + }); + } + + private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { + try { + final int baseRank = Ranking.getBaseRankForAuthority(authority); + + 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, baseRank); + + final Uri uriForRawData = buildUriForRawData(authority); + addIndexablesForRawDataUri(context, packageName, uriForRawData, + SearchIndexablesContract.INDEXABLES_RAW_COLUMNS, baseRank); + return true; + } catch (PackageManager.NameNotFoundException e) { + Log.w(LOG_TAG, "Could not create context for " + packageName + ": " + + Log.getStackTraceString(e)); + return false; + } + } + + private 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; + } + + 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); + result.add(key); + } + } + return result; + } finally { + cursor.close(); + } + } + + public void addIndexableData(SearchIndexableData data) { + synchronized (mDataToProcess) { + mDataToProcess.dataToUpdate.add(data); + } + } + + public void deleteIndexableData(SearchIndexableData data) { + synchronized (mDataToProcess) { + mDataToProcess.dataToDelete.add(data); + } + } + + public void addNonIndexableKeys(String authority, List keys) { + synchronized (mDataToProcess) { + mDataToProcess.nonIndexableKeys.put(authority, keys); + } + } + + private void updateFromRemoteProvider(String packageName, String authority) { + if (addIndexablesFromRemoteProvider(packageName, authority)) { + updateInternal(); + } + } + + /** + * Update the Index for a specific class name resources + * + * @param className the class name (typically a fragment name). + * @param rebuild true means that you want to delete the data from the Index first. + * @param includeInSearchResults true means that you want the bit "enabled" set so that the + * data will be seen included into the search results + */ + public void updateFromClassNameResource(String className, final boolean rebuild, + boolean includeInSearchResults) { + if (className == null) { + throw new IllegalArgumentException("class name cannot be null!"); + } + final SearchIndexableResource res = SearchIndexableResources.getResourceByName(className); + if (res == null ) { + Log.e(LOG_TAG, "Cannot find SearchIndexableResources for class name: " + className); + return; + } + res.context = mContext; + res.enabled = includeInSearchResults; + AsyncTask.execute(new Runnable() { + @Override + public void run() { + if (rebuild) { + deleteIndexableData(res); + } + addIndexableData(res); + mDataToProcess.forceUpdate = true; + updateInternal(); + res.enabled = false; + } + }); + } + + public void updateFromSearchIndexableData(final SearchIndexableData data) { + AsyncTask.execute(new Runnable() { + @Override + public void run() { + addIndexableData(data); + mDataToProcess.forceUpdate = true; + updateInternal(); + } + }); + } + + private SQLiteDatabase getReadableDatabase() { + return IndexDatabaseHelper.getInstance(mContext).getReadableDatabase(); + } + + private SQLiteDatabase getWritableDatabase() { + try { + return IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); + } catch (SQLiteException e) { + Log.e(LOG_TAG, "Cannot open writable database", e); + return null; + } + } + + 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 updateInternal() { + synchronized (mDataToProcess) { + final DatabaseIndexingManager.UpdateIndexTask task = + new DatabaseIndexingManager.UpdateIndexTask(); + DatabaseIndexingManager.UpdateData copy = mDataToProcess.copy(); + task.execute(copy); + mDataToProcess.clear(); + } + } + + private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, + Uri uri, String[] projection, int baseRank) { + + 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_XML_RES_RANK); + final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; + + 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.rank = rank; + 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, int baseRank) { + + 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); + final int rank = (providerRank > 0) ? baseRank + providerRank : baseRank; + + 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.rank = rank; + 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(); + } + } + + private void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, + SearchIndexableData data, Map> nonIndexableKeys) { + if (data instanceof SearchIndexableResource) { + indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); + } else if (data instanceof SearchIndexableRaw) { + indexOneRaw(database, localeStr, (SearchIndexableRaw) data); + } + } + + private void indexOneRaw(SQLiteDatabase database, String localeStr, + SearchIndexableRaw raw) { + // Should be the same locale as the one we are processing + if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { + return; + } + + updateOneRowWithFilteredData(database, localeStr, + raw.title, + raw.summaryOn, + raw.summaryOff, + raw.entries, + raw.className, + raw.screenTitle, + raw.iconResId, + raw.rank, + raw.keywords, + raw.intentAction, + raw.intentTargetPackage, + raw.intentTargetClass, + raw.enabled, + raw.key, + raw.userId); + } + + private void indexOneResource(SQLiteDatabase database, String localeStr, + SearchIndexableResource sir, Map> nonIndexableKeysFromResource) { + + if (sir == null) { + Log.e(LOG_TAG, "Cannot index a null resource!"); + return; + } + + final List nonIndexableKeys = new ArrayList(); + + if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) { + List resNonIndxableKeys = nonIndexableKeysFromResource.get(sir.packageName); + if (resNonIndxableKeys != null && resNonIndxableKeys.size() > 0) { + nonIndexableKeys.addAll(resNonIndxableKeys); + } + + indexFromResource(sir.context, database, localeStr, + sir.xmlResId, sir.className, sir.iconResId, sir.rank, + sir.intentAction, sir.intentTargetPackage, sir.intentTargetClass, + nonIndexableKeys); + } else { + if (TextUtils.isEmpty(sir.className)) { + Log.w(LOG_TAG, "Cannot index an empty Search Provider name!"); + return; + } + + final Class clazz = DatabaseIndexingUtils.getIndexableClass(sir.className); + if (clazz == null) { + Log.d(LOG_TAG, "SearchIndexableResource '" + sir.className + + "' should implement the " + Indexable.class.getName() + " interface!"); + return; + } + + // Will be non null only for a Local provider implementing a + // SEARCH_INDEX_DATA_PROVIDER field + final Indexable.SearchIndexProvider provider = + DatabaseIndexingUtils.getSearchIndexProvider(clazz); + if (provider != null) { + List providerNonIndexableKeys = provider.getNonIndexableKeys(sir.context); + if (providerNonIndexableKeys != null && providerNonIndexableKeys.size() > 0) { + nonIndexableKeys.addAll(providerNonIndexableKeys); + } + + indexFromProvider(mContext, database, localeStr, provider, sir.className, + sir.iconResId, sir.rank, sir.enabled, nonIndexableKeys); + } + } + } + + private void indexFromResource(Context context, SQLiteDatabase database, String localeStr, + int xmlResId, String fragmentName, int iconResId, int rank, + String intentAction, String intentTargetPackage, String intentTargetClass, + List nonIndexableKeys) { + + XmlResourceParser parser = null; + try { + parser = context.getResources().getXml(xmlResId); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Parse next until start tag is found + } + + String nodeName = parser.getName(); + if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) { + throw new RuntimeException( + "XML document must start with tag; found" + + nodeName + " at " + parser.getPositionDescription()); + } + + final int outerDepth = parser.getDepth(); + final AttributeSet attrs = Xml.asAttributeSet(parser); + + final String screenTitle = XMLParserUtil.getDataTitle(context, attrs); + + String key = XMLParserUtil.getDataKey(context, attrs); + + String title; + String summary; + String keywords; + + // Insert rows for the main PreferenceScreen node. Rewrite the data for removing + // hyphens. + if (!nonIndexableKeys.contains(key)) { + title = XMLParserUtil.getDataTitle(context, attrs); + summary = XMLParserUtil.getDataSummary(context, attrs); + keywords = XMLParserUtil.getDataKeywords(context, attrs); + + updateOneRowWithFilteredData(database, localeStr, title, summary, null, null, + fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, true, + key, -1 /* default user id */); + } + + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + nodeName = parser.getName(); + + key = XMLParserUtil.getDataKey(context, attrs); + if (nonIndexableKeys.contains(key)) { + continue; + } + + title = XMLParserUtil.getDataTitle(context, attrs); + keywords = XMLParserUtil.getDataKeywords(context, attrs); + + if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) { + summary = XMLParserUtil.getDataSummary(context, attrs); + + String entries = null; + + if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) { + entries = XMLParserUtil.getDataEntries(context, attrs); + } + + // Insert rows for the child nodes of PreferenceScreen + updateOneRowWithFilteredData(database, localeStr, title, summary, null, entries, + fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, + true, key, -1 /* default user id */); + } else { + String summaryOn = XMLParserUtil.getDataSummaryOn(context, attrs); + String summaryOff = XMLParserUtil.getDataSummaryOff(context, attrs); + + if (TextUtils.isEmpty(summaryOn) && TextUtils.isEmpty(summaryOff)) { + summaryOn = XMLParserUtil.getDataSummary(context, attrs); + } + + updateOneRowWithFilteredData(database, localeStr, title, summaryOn, summaryOff, + null, fragmentName, screenTitle, iconResId, rank, + keywords, intentAction, intentTargetPackage, intentTargetClass, + true, key, -1 /* default user id */); + } + } + + } catch (XmlPullParserException e) { + throw new RuntimeException("Error parsing PreferenceScreen", e); + } catch (IOException e) { + throw new RuntimeException("Error parsing PreferenceScreen", e); + } finally { + if (parser != null) parser.close(); + } + } + + private void indexFromProvider(Context context, SQLiteDatabase database, String localeStr, + Indexable.SearchIndexProvider provider, String className, int iconResId, int rank, + boolean enabled, List nonIndexableKeys) { + + if (provider == null) { + Log.w(LOG_TAG, "Cannot find provider: " + className); + return; + } + + final List rawList = provider.getRawDataToIndex(context, enabled); + + if (rawList != null) { + final int rawSize = rawList.size(); + for (int i = 0; i < rawSize; i++) { + SearchIndexableRaw raw = rawList.get(i); + + // Should be the same locale as the one we are processing + if (!raw.locale.toString().equalsIgnoreCase(localeStr)) { + continue; + } + + if (nonIndexableKeys.contains(raw.key)) { + continue; + } + + updateOneRowWithFilteredData(database, localeStr, + raw.title, + raw.summaryOn, + raw.summaryOff, + raw.entries, + className, + raw.screenTitle, + iconResId, + rank, + raw.keywords, + raw.intentAction, + raw.intentTargetPackage, + raw.intentTargetClass, + raw.enabled, + raw.key, + raw.userId); + } + } + + final List resList = + provider.getXmlResourcesToIndex(context, enabled); + if (resList != null) { + final int resSize = resList.size(); + for (int i = 0; i < resSize; i++) { + SearchIndexableResource item = resList.get(i); + + // Should be the same locale as the one we are processing + if (!item.locale.toString().equalsIgnoreCase(localeStr)) { + continue; + } + + final int itemIconResId = (item.iconResId == 0) ? iconResId : item.iconResId; + final int itemRank = (item.rank == 0) ? rank : item.rank; + String itemClassName = (TextUtils.isEmpty(item.className)) + ? className : item.className; + + indexFromResource(context, database, localeStr, + item.xmlResId, itemClassName, itemIconResId, itemRank, + item.intentAction, item.intentTargetPackage, + item.intentTargetClass, nonIndexableKeys); + } + } + } + + private void updateOneRowWithFilteredData(SQLiteDatabase database, String locale, + String title, String summaryOn, String summaryOff, String entries, + String className, + String screenTitle, int iconResId, int rank, String keywords, + String intentAction, String intentTargetPackage, String intentTargetClass, + boolean enabled, String key, int userId) { + + final String updatedTitle = XMLParserUtil.normalizeHyphen(title); + final String updatedSummaryOn = XMLParserUtil.normalizeHyphen(summaryOn); + final String updatedSummaryOff = XMLParserUtil.normalizeHyphen(summaryOff); + + final String normalizedTitle = XMLParserUtil.normalizeString(updatedTitle); + final String normalizedSummaryOn = XMLParserUtil.normalizeString(updatedSummaryOn); + final String normalizedSummaryOff = XMLParserUtil.normalizeString(updatedSummaryOff); + + final String spaceDelimitedKeywords = XMLParserUtil.normalizeKeywords(keywords); + + updateOneRow(database, locale, + updatedTitle, normalizedTitle, updatedSummaryOn, normalizedSummaryOn, + updatedSummaryOff, normalizedSummaryOff, entries, className, screenTitle, iconResId, + rank, spaceDelimitedKeywords, intentAction, intentTargetPackage, intentTargetClass, + enabled, key, userId); + } + + private void updateOneRow(SQLiteDatabase database, String locale, String updatedTitle, + String normalizedTitle, String updatedSummaryOn, String normalizedSummaryOn, + String updatedSummaryOff, String normalizedSummaryOff, String entries, String className, + String screenTitle, int iconResId, int rank, String spaceDelimitedKeywords, + String intentAction, String intentTargetPackage, String intentTargetClass, + boolean enabled, String key, int userId) { + + if (TextUtils.isEmpty(updatedTitle)) { + return; + } + + // The DocID should contains more than the title string itself (you may have two settings + // with the same title). So we need to use a combination of the title and the screenTitle. + StringBuilder sb = new StringBuilder(updatedTitle); + sb.append(screenTitle); + int docId = sb.toString().hashCode(); + + ContentValues values = new ContentValues(); + values.put(IndexDatabaseHelper.IndexColumns.DOCID, docId); + values.put(IndexDatabaseHelper.IndexColumns.LOCALE, locale); + values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, rank); + values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, updatedTitle); + values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED, normalizedTitle); + values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON, updatedSummaryOn); + values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_ON_NORMALIZED, + normalizedSummaryOn); + values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF, updatedSummaryOff); + values.put(IndexDatabaseHelper.IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, + normalizedSummaryOff); + values.put(IndexDatabaseHelper.IndexColumns.DATA_ENTRIES, entries); + values.put(IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS, spaceDelimitedKeywords); + values.put(IndexDatabaseHelper.IndexColumns.CLASS_NAME, className); + values.put(IndexDatabaseHelper.IndexColumns.SCREEN_TITLE, screenTitle); + values.put(IndexDatabaseHelper.IndexColumns.INTENT_ACTION, intentAction); + values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_PACKAGE, intentTargetPackage); + values.put(IndexDatabaseHelper.IndexColumns.INTENT_TARGET_CLASS, intentTargetClass); + values.put(IndexDatabaseHelper.IndexColumns.ICON, iconResId); + values.put(IndexDatabaseHelper.IndexColumns.ENABLED, enabled); + values.put(IndexDatabaseHelper.IndexColumns.DATA_KEY_REF, key); + values.put(IndexDatabaseHelper.IndexColumns.USER_ID, userId); + + database.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); + } + + /** + * A private class for updating the Index database + */ + private class UpdateIndexTask extends AsyncTask { + + @Override + protected void onPreExecute() { + super.onPreExecute(); + mIsAvailable.set(false); + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + mIsAvailable.set(true); + } + + @Override + protected Void doInBackground(DatabaseIndexingManager.UpdateData... params) { + try { + final List dataToUpdate = params[0].dataToUpdate; + final List dataToDelete = params[0].dataToDelete; + final Map> nonIndexableKeys = params[0].nonIndexableKeys; + + final boolean forceUpdate = params[0].forceUpdate; + final boolean fullIndex = params[0].fullIndex; + + final SQLiteDatabase database = getWritableDatabase(); + if (database == null) { + Log.e(LOG_TAG, "Cannot update Index as I cannot get a writable database"); + return null; + } + final String localeStr = Locale.getDefault().toString(); + + try { + database.beginTransaction(); + if (dataToDelete.size() > 0) { + processDataToDelete(database, localeStr, dataToDelete); + } + if (dataToUpdate.size() > 0) { + processDataToUpdate(database, localeStr, dataToUpdate, nonIndexableKeys, + forceUpdate); + } + database.setTransactionSuccessful(); + } finally { + database.endTransaction(); + } + if (fullIndex) { + IndexDatabaseHelper.setLocaleIndexed(mContext, localeStr); + } + } catch (SQLiteFullException e) { + Log.e(LOG_TAG, "Unable to index search, out of space", e); + } + + return null; + } + + private boolean processDataToUpdate(SQLiteDatabase database, String localeStr, + List dataToUpdate, Map> nonIndexableKeys, + boolean forceUpdate) { + + if (!forceUpdate && IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)) { + Log.d(LOG_TAG, "Locale '" + localeStr + "' is already indexed"); + return true; + } + + boolean result = false; + final long current = System.currentTimeMillis(); + + final int count = dataToUpdate.size(); + for (int n = 0; n < count; n++) { + final SearchIndexableData data = dataToUpdate.get(n); + try { + indexOneSearchIndexableData(database, localeStr, data, nonIndexableKeys); + } catch (Exception e) { + Log.e(LOG_TAG, "Cannot index: " + (data != null ? data.className : data) + + " for locale: " + localeStr, e); + } + } + + final long now = System.currentTimeMillis(); + Log.d(LOG_TAG, "Indexing locale '" + localeStr + "' took " + + (now - current) + " millis"); + return result; + } + + private boolean processDataToDelete(SQLiteDatabase database, String localeStr, + List dataToDelete) { + + boolean result = false; + final long current = System.currentTimeMillis(); + + final int count = dataToDelete.size(); + for (int n = 0; n < count; n++) { + final SearchIndexableData data = dataToDelete.get(n); + if (data == null) { + continue; + } + if (!TextUtils.isEmpty(data.className)) { + delete(database, IndexDatabaseHelper.IndexColumns.CLASS_NAME, data.className); + } else { + if (data instanceof SearchIndexableRaw) { + final SearchIndexableRaw raw = (SearchIndexableRaw) data; + if (!TextUtils.isEmpty(raw.title)) { + delete(database, IndexDatabaseHelper.IndexColumns.DATA_TITLE, + raw.title); + } + } + } + } + + final long now = System.currentTimeMillis(); + Log.d(LOG_TAG, "Deleting data for locale '" + localeStr + "' took " + + (now - current) + " millis"); + return result; + } + + private int delete(SQLiteDatabase database, String columName, String value) { + final String whereClause = columName + "=?"; + final String[] whereArgs = new String[] { value }; + + return database.delete(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, whereClause, + whereArgs); + } + } +} diff --git a/src/com/android/settings/search2/DatabaseIndexingUtils.java b/src/com/android/settings/search2/DatabaseIndexingUtils.java new file mode 100644 index 00000000000..a8f64dfeaa8 --- /dev/null +++ b/src/com/android/settings/search2/DatabaseIndexingUtils.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.Manifest; +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.text.TextUtils; +import android.util.Log; + +import com.android.settings.search.Indexable; + +import java.lang.reflect.Field; + +/** + * Utility class for {@like DatabaseIndexingManager} to handle the mapping between Payloads + * and Preference controllers, and managing indexable classes. + */ +public class DatabaseIndexingUtils { + + private static final String LOG_TAG = "IndexingUtil"; + + private static final String FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER = + "SEARCH_INDEX_DATA_PROVIDER"; + + public static Class getIndexableClass(String className) { + final Class clazz; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + Log.d(LOG_TAG, "Cannot find class: " + className); + return null; + } + return isIndexableClass(clazz) ? clazz : null; + } + + public static boolean isIndexableClass(final Class clazz) { + return (clazz != null) && Indexable.class.isAssignableFrom(clazz); + } + + public static Indexable.SearchIndexProvider getSearchIndexProvider(final Class clazz) { + try { + final Field f = clazz.getField(FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER); + return (Indexable.SearchIndexProvider) f.get(null); + } catch (NoSuchFieldException e) { + Log.d(LOG_TAG, "Cannot find field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (SecurityException se) { + Log.d(LOG_TAG, + "Security exception for field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (IllegalAccessException e) { + Log.d(LOG_TAG, + "Illegal access to field '" + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } catch (IllegalArgumentException e) { + Log.d(LOG_TAG, + "Illegal argument when accessing field '" + + FIELD_NAME_SEARCH_INDEX_DATA_PROVIDER + "'"); + } + 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 + */ + public 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); + } + + public 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/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java index da29c85da4e..ad26eae06d8 100644 --- a/src/com/android/settings/search2/SearchFeatureProvider.java +++ b/src/com/android/settings/search2/SearchFeatureProvider.java @@ -46,4 +46,14 @@ public interface SearchFeatureProvider { * Returns a new loader to search installed apps. */ InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query); + + /** + * Returns the manager for indexing Settings data. + */ + DatabaseIndexingManager getIndexingManager(Context context); + + /** + * Updates the Settings indexes + */ + void updateIndex(Context context); } diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java index 72030493375..e2d25ad83ea 100644 --- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java @@ -23,6 +23,8 @@ import android.view.Menu; import android.view.MenuItem; import com.android.settings.R; +import com.android.settings.search.Index; + import com.android.settings.applications.PackageManagerWrapperImpl; /** @@ -31,6 +33,7 @@ import com.android.settings.applications.PackageManagerWrapperImpl; public class SearchFeatureProviderImpl implements SearchFeatureProvider { protected Context mContext; + private DatabaseIndexingManager mDatabaseIndexingManager; public SearchFeatureProviderImpl(Context context) { mContext = context; @@ -71,4 +74,22 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { return new InstalledAppResultLoader( context, new PackageManagerWrapperImpl(context.getPackageManager()), query); } + + @Override + public DatabaseIndexingManager getIndexingManager(Context context) { + if (mDatabaseIndexingManager == null) { + mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext(), + context.getPackageName()); + } + return mDatabaseIndexingManager; + } + + @Override + public void updateIndex(Context context) { + if (isEnabled()) { + getIndexingManager(context).update(); + } else { + Index.getInstance(context).update(); + } + } } diff --git a/src/com/android/settings/search2/XMLParserUtil.java b/src/com/android/settings/search2/XMLParserUtil.java new file mode 100644 index 00000000000..dcb3cbbb70a --- /dev/null +++ b/src/com/android/settings/search2/XMLParserUtil.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2016 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.search2; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.TypedValue; + +import com.android.settings.R; + +import java.text.Normalizer; +import java.util.regex.Pattern; + +/** + * Utility class to parse elements of XML preferences + */ +public class XMLParserUtil { + + private static final String NON_BREAKING_HYPHEN = "\u2011"; + private static final String EMPTY = ""; + private static final String LIST_DELIMITERS = "[,]\\s*"; + private static final String HYPHEN = "-"; + private static final String SPACE = " "; + + private static final String ENTRIES_SEPARATOR = "|"; + + private static final Pattern REMOVE_DIACRITICALS_PATTERN + = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); + + public static String getDataKey(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_key); + } + + public static String getDataTitle(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_title); + } + + public static String getDataSummary(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.Preference, + com.android.internal.R.styleable.Preference_summary); + } + + public static String getDataSummaryOn(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.CheckBoxPreference, + com.android.internal.R.styleable.CheckBoxPreference_summaryOn); + } + + public static String getDataSummaryOff(Context context, AttributeSet attrs) { + return getData(context, attrs, + com.android.internal.R.styleable.CheckBoxPreference, + com.android.internal.R.styleable.CheckBoxPreference_summaryOff); + } + + public static String getDataEntries(Context context, AttributeSet attrs) { + return getDataEntries(context, attrs, + com.android.internal.R.styleable.ListPreference, + com.android.internal.R.styleable.ListPreference_entries); + } + + public static String getDataKeywords(Context context, AttributeSet attrs) { + return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); + } + + public static String getData(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + CharSequence data = null; + if (tv != null && tv.type == TypedValue.TYPE_STRING) { + if (tv.resourceId != 0) { + data = context.getText(tv.resourceId); + } else { + data = tv.string; + } + } + return (data != null) ? data.toString() : null; + } + + public static String getDataEntries(Context context, AttributeSet set, int[] attrs, int resId) { + final TypedArray sa = context.obtainStyledAttributes(set, attrs); + final TypedValue tv = sa.peekValue(resId); + + String[] data = null; + if (tv != null && tv.type == TypedValue.TYPE_REFERENCE) { + if (tv.resourceId != 0) { + data = context.getResources().getStringArray(tv.resourceId); + } + } + final int count = (data == null ) ? 0 : data.length; + if (count == 0) { + return null; + } + final StringBuilder result = new StringBuilder(); + for (int n = 0; n < count; n++) { + result.append(data[n]); + result.append(ENTRIES_SEPARATOR); + } + return result.toString(); + } + + public static String normalizeHyphen(String input) { + return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; + } + + public static String normalizeString(String input) { + final String nohyphen = (input != null) ? input.replaceAll(HYPHEN, EMPTY) : EMPTY; + final String normalized = Normalizer.normalize(nohyphen, Normalizer.Form.NFD); + + return REMOVE_DIACRITICALS_PATTERN.matcher(normalized).replaceAll("").toLowerCase(); + } + + public static String normalizeKeywords(String input) { + return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; + } +} diff --git a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java index 67189d09072..d9e2dd6b195 100644 --- a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java @@ -18,12 +18,18 @@ package com.android.settings.search; import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; import android.view.Menu; import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search2.DatabaseIndexingManager; import com.android.settings.search2.SearchFeatureProviderImpl; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settingslib.drawer.DashboardCategory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,9 +39,14 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.annotation.Config; +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.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @@ -64,4 +75,22 @@ public class SearchFeatureProviderImplTest { verify(menu).add(anyInt(), anyInt(), anyInt(), anyString()); } + + @Test + public void testUpdateIndexNewSearch_UsesDatabaseIndexingManager() { + mProvider = spy(new SearchFeatureProviderImpl(mActivity)); + when(mProvider.isEnabled()).thenReturn(true); + + mProvider.updateIndex(mActivity); + verify(mProvider).getIndexingManager(any(Context.class)); + } + + @Test + public void testUpdateIndexNewSearch_UsesIndex() { + mProvider = spy(new SearchFeatureProviderImpl(mActivity)); + when(mProvider.isEnabled()).thenReturn(false); + + mProvider.updateIndex(mActivity); + verify(mProvider, never()).getIndexingManager(any(Context.class)); + } }