First step in refactoring Index.java

Handles the following refators:
 - Indexing code into: DatabaseIndexingManager
 - Indexing utility methods into: DatabaseIndexingUtil
 - XML Parsiing utility methods into XMLParserUtil

Bug: 33451851
Test: make RunSettingsRoboTests
Change-Id: I4264ad3806d1bd3a66d879c16ad6c8315ecb832b
This commit is contained in:
Matthew Fritze
2016-12-08 09:54:13 -08:00
parent d4b43228b0
commit 18fcb08308
7 changed files with 1280 additions and 2 deletions

View File

@@ -253,7 +253,7 @@ public class SettingsActivity extends SettingsDrawerActivity
String action = intent.getAction(); String action = intent.getAction();
if (action.equals(Intent.ACTION_USER_ADDED) if (action.equals(Intent.ACTION_USER_ADDED)
|| action.equals(Intent.ACTION_USER_REMOVED)) { || action.equals(Intent.ACTION_USER_REMOVED)) {
Index.getInstance(getApplicationContext()).update(); mSearchFeatureProvider.updateIndex(getApplicationContext());
} }
} }
}; };
@@ -310,7 +310,7 @@ public class SettingsActivity extends SettingsDrawerActivity
@Override @Override
public void onConfigurationChanged(Configuration newConfig) { public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
Index.getInstance(this).update(); mSearchFeatureProvider.updateIndex(getApplicationContext());
} }
@Override @Override

View File

@@ -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<String> EMPTY_LIST = Collections.<String>emptyList();
private final String mBaseAuthority;
/**
* A private class to describe the update data for the Index database
*/
private static class UpdateData {
public List<SearchIndexableData> dataToUpdate;
public List<SearchIndexableData> dataToDelete;
public Map<String, List<String>> nonIndexableKeys;
public boolean forceUpdate;
public boolean fullIndex;
public UpdateData() {
dataToUpdate = new ArrayList<SearchIndexableData>();
dataToDelete = new ArrayList<SearchIndexableData>();
nonIndexableKeys = new HashMap<String, List<String>>();
}
public UpdateData(DatabaseIndexingManager.UpdateData other) {
dataToUpdate = new ArrayList<SearchIndexableData>(other.dataToUpdate);
dataToDelete = new ArrayList<SearchIndexableData>(other.dataToDelete);
nonIndexableKeys = new HashMap<String, List<String>>(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<ResolveInfo> 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<String> keys =
getNonIndexablesKeysFromRemoteProvider(packageName, authority);
addNonIndexableKeys(packageName, keys);
}
private List<String> 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<String> 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<String> result = new ArrayList<String>();
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<String> 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<String, List<String>> 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<String, List<String>> nonIndexableKeysFromResource) {
if (sir == null) {
Log.e(LOG_TAG, "Cannot index a null resource!");
return;
}
final List<String> nonIndexableKeys = new ArrayList<String>();
if (sir.xmlResId > SearchIndexableResources.NO_DATA_RES_ID) {
List<String> 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<String> 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<String> 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 <PreferenceScreen> 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<String> nonIndexableKeys) {
if (provider == null) {
Log.w(LOG_TAG, "Cannot find provider: " + className);
return;
}
final List<SearchIndexableRaw> 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<SearchIndexableResource> 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<DatabaseIndexingManager.UpdateData, Integer,
Void> {
@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<SearchIndexableData> dataToUpdate = params[0].dataToUpdate;
final List<SearchIndexableData> dataToDelete = params[0].dataToDelete;
final Map<String, List<String>> 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<SearchIndexableData> dataToUpdate, Map<String, List<String>> 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<SearchIndexableData> 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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -46,4 +46,14 @@ public interface SearchFeatureProvider {
* Returns a new loader to search installed apps. * Returns a new loader to search installed apps.
*/ */
InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query); 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);
} }

View File

@@ -23,6 +23,8 @@ import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.search.Index;
import com.android.settings.applications.PackageManagerWrapperImpl; import com.android.settings.applications.PackageManagerWrapperImpl;
/** /**
@@ -31,6 +33,7 @@ import com.android.settings.applications.PackageManagerWrapperImpl;
public class SearchFeatureProviderImpl implements SearchFeatureProvider { public class SearchFeatureProviderImpl implements SearchFeatureProvider {
protected Context mContext; protected Context mContext;
private DatabaseIndexingManager mDatabaseIndexingManager;
public SearchFeatureProviderImpl(Context context) { public SearchFeatureProviderImpl(Context context) {
mContext = context; mContext = context;
@@ -71,4 +74,22 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
return new InstalledAppResultLoader( return new InstalledAppResultLoader(
context, new PackageManagerWrapperImpl(context.getPackageManager()), query); 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();
}
}
} }

View File

@@ -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;
}
}

View File

@@ -18,12 +18,18 @@
package com.android.settings.search; package com.android.settings.search;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.view.Menu; import android.view.Menu;
import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig; 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.search2.SearchFeatureProviderImpl;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settingslib.drawer.DashboardCategory;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -33,9 +39,14 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.annotation.Config; 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.anyInt;
import static org.mockito.Matchers.anyString; 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.verify;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -64,4 +75,22 @@ public class SearchFeatureProviderImplTest {
verify(menu).add(anyInt(), anyInt(), anyInt(), anyString()); 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));
}
} }