Move search querying into a single API

Settings now collects search results from a single
loader which fetches from an aggregator. This is to
facilitate the separation of search functionalitiy,
where "query" becomes a single synchronous call.
In this case, the aggregator will move to the
unbundled app and would be called on the
other end of the Query call. i.e. the new search
result loader will just call query, and unbundled
search will handle everything else.

An important implication is that the results will
be returned in a ranked order. Thus the ranking and
merging logic has been moved out of the RecyclerView
adapter (which is a good clean-up, anyway).

The SearchResultAggregator starts a Future for each
of the data sources:
- Static Results
- Installed Apps
- Input Devices
- Accessibility Services

We allow up to 500ms to collect the static results,
and then an additional 150ms for each subsequent
loader. In my quick tests, the static results take
about 20-30ms to load. The longest loader is installed
apps which takes roughly 50-60ms seconds (note that
this will be improved with dynamic result caching).

To handle the ranking in DatabaseResultLoader,
we start a Future to collect the dynamic ranking before
we start the SQL queries. When the SQL is done, we
wait the same timeout as before. Then we merge the
results, as before.

For now we have not changed how the Dynamic results
are collected, but eventually they will be a cache
of dynamic results.

Bug: 33577327
Bug: 67360547
Test: robotests
Change-Id: I91fb03f9fd059672a970f48bea21c8d655007fa3
This commit is contained in:
Matthew Fritze
2017-10-05 16:13:36 -07:00
parent 10d0518fa3
commit fb772248b1
26 changed files with 1590 additions and 1878 deletions

View File

@@ -30,105 +30,112 @@ import android.os.UserHandle;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.util.IconDrawableFactory; import android.util.IconDrawableFactory;
import android.util.Log;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.accessibility.AccessibilitySettings; import com.android.settings.accessibility.AccessibilitySettings;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.utils.AsyncLoader;
import java.util.HashSet; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends SearchResult>> { public class AccessibilityServiceResultLoader extends
FutureTask<List<? extends SearchResult>> {
private static final int NAME_NO_MATCH = -1;
private final Context mContext;
private List<String> mBreadcrumb;
private SiteMapManager mSiteMapManager;
@VisibleForTesting
final String mQuery;
private final AccessibilityManager mAccessibilityManager;
private final PackageManager mPackageManager;
private final int mUserId;
private static final String TAG = "A11yResultFutureTask";
public AccessibilityServiceResultLoader(Context context, String query, public AccessibilityServiceResultLoader(Context context, String query,
SiteMapManager mapManager) { SiteMapManager manager) {
super(context); super(new AccessibilityServiceResultCallable(context, query, manager));
mContext = context;
mUserId = UserHandle.myUserId();
mSiteMapManager = mapManager;
mPackageManager = context.getPackageManager();
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mQuery = query;
} }
@Override static class AccessibilityServiceResultCallable implements
public Set<? extends SearchResult> loadInBackground() { Callable<List<? extends SearchResult>> {
final Set<SearchResult> results = new HashSet<>();
final Context context = getContext();
final List<AccessibilityServiceInfo> services = mAccessibilityManager
.getInstalledAccessibilityServiceList();
final IconDrawableFactory iconFactory = IconDrawableFactory.newInstance(mContext);
final String screenTitle = context.getString(R.string.accessibility_settings);
for (AccessibilityServiceInfo service : services) {
if (service == null) {
continue;
}
final ResolveInfo resolveInfo = service.getResolveInfo();
if (service.getResolveInfo() == null) {
continue;
}
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
final CharSequence title = resolveInfo.loadLabel(mPackageManager);
final int wordDiff = getWordDifference(title.toString(), mQuery);
if (wordDiff == NAME_NO_MATCH) {
continue;
}
final Drawable icon;
if (resolveInfo.getIconResource() == 0) {
icon = ContextCompat.getDrawable(context, R.mipmap.ic_accessibility_generic);
} else {
icon = iconFactory.getBadgedIcon(
resolveInfo.serviceInfo,
resolveInfo.serviceInfo.applicationInfo,
mUserId);
}
final String componentName = new ComponentName(serviceInfo.packageName,
serviceInfo.name).flattenToString();
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
AccessibilitySettings.class.getName(), componentName, screenTitle);
results.add(new SearchResult.Builder() private static final int NAME_NO_MATCH = -1;
.setTitle(title)
.addBreadcrumbs(getBreadCrumb()) private final Context mContext;
.setPayload(new ResultPayload(intent)) private List<String> mBreadcrumb;
.setRank(wordDiff) private SiteMapManager mSiteMapManager;
.setIcon(icon) @VisibleForTesting
.setStableId(Objects.hash(screenTitle, componentName)) final String mQuery;
.build()); private final AccessibilityManager mAccessibilityManager;
private final PackageManager mPackageManager;
private final int mUserId;
public AccessibilityServiceResultCallable(Context context, String query,
SiteMapManager mapManager) {
mUserId = UserHandle.myUserId();
mContext = context;
mSiteMapManager = mapManager;
mPackageManager = context.getPackageManager();
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
mQuery = query;
} }
return results;
}
private List<String> getBreadCrumb() { @Override
if (mBreadcrumb == null || mBreadcrumb.isEmpty()) { public List<? extends SearchResult> call() throws Exception {
final Context context = getContext(); long startTime = System.currentTimeMillis();
mBreadcrumb = mSiteMapManager.buildBreadCrumb( final List<SearchResult> results = new ArrayList<>();
context, AccessibilitySettings.class.getName(), final List<AccessibilityServiceInfo> services = mAccessibilityManager
context.getString(R.string.accessibility_settings)); .getInstalledAccessibilityServiceList();
final IconDrawableFactory iconFactory = IconDrawableFactory.newInstance(mContext);
final String screenTitle = mContext.getString(R.string.accessibility_settings);
for (AccessibilityServiceInfo service : services) {
if (service == null) {
continue;
}
final ResolveInfo resolveInfo = service.getResolveInfo();
if (service.getResolveInfo() == null) {
continue;
}
final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
final CharSequence title = resolveInfo.loadLabel(mPackageManager);
final int wordDiff = getWordDifference(title.toString(), mQuery);
if (wordDiff == NAME_NO_MATCH) {
continue;
}
final Drawable icon;
if (resolveInfo.getIconResource() == 0) {
icon = ContextCompat.getDrawable(mContext, R.mipmap.ic_accessibility_generic);
} else {
icon = iconFactory.getBadgedIcon(
resolveInfo.serviceInfo,
resolveInfo.serviceInfo.applicationInfo,
mUserId);
}
final String componentName = new ComponentName(serviceInfo.packageName,
serviceInfo.name).flattenToString();
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext,
AccessibilitySettings.class.getName(), componentName, screenTitle);
results.add(new SearchResult.Builder()
.setTitle(title)
.addBreadcrumbs(getBreadCrumb())
.setPayload(new ResultPayload(intent))
.setRank(wordDiff)
.setIcon(icon)
.setStableId(Objects.hash(screenTitle, componentName))
.build());
}
Collections.sort(results);
Log.i(TAG, "A11y search loading took:" + (System.currentTimeMillis() - startTime));
return results;
} }
return mBreadcrumb;
}
@Override
protected void onDiscardResult(Set<? extends SearchResult> result) {
private List<String> getBreadCrumb() {
if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
mBreadcrumb = mSiteMapManager.buildBreadCrumb(
mContext, AccessibilitySettings.class.getName(),
mContext.getString(R.string.accessibility_settings));
}
return mBreadcrumb;
}
} }
} }

View File

@@ -36,16 +36,6 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import static com.android.settings.search.DatabaseResultLoader.BASE_RANKS; import static com.android.settings.search.DatabaseResultLoader.BASE_RANKS;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_CLASS_NAME;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ICON;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD_TYPE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_SCREEN_TITLE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_SUMMARY_ON;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_TITLE;
import static com.android.settings.search.SearchResult.TOP_RANK; import static com.android.settings.search.SearchResult.TOP_RANK;
/** /**
@@ -62,6 +52,25 @@ public class CursorToSearchResultConverter {
private static final String TAG = "CursorConverter"; private static final String TAG = "CursorConverter";
/**
* These indices are used to match the columns of the this loader's SELECT statement.
* These are not necessarily the same order nor similar coverage as the schema defined in
* IndexDatabaseHelper
*/
public static final int COLUMN_INDEX_ID = 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_CLASS_NAME = 4;
public static final int COLUMN_INDEX_SCREEN_TITLE = 5;
public static final int COLUMN_INDEX_ICON = 6;
public static final int COLUMN_INDEX_INTENT_ACTION = 7;
public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
public static final int COLUMN_INDEX_KEY = 10;
public static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
public static final int COLUMN_INDEX_PAYLOAD = 12;
private final Context mContext; private final Context mContext;
private final int LONG_TITLE_LENGTH = 20; private final int LONG_TITLE_LENGTH = 20;

View File

@@ -17,11 +17,13 @@
package com.android.settings.search; package com.android.settings.search;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
import static com.android.settings.search.DatabaseResultLoader import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_ID;
import static com.android.settings.search.CursorToSearchResultConverter
.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE; .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY; import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_KEY;
import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS; import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
@@ -31,7 +33,6 @@ import static com.android.settings.search.IndexDatabaseHelper.IndexColumns
.DATA_SUMMARY_ON_NORMALIZED; .DATA_SUMMARY_ON_NORMALIZED;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION; import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;

View File

@@ -24,35 +24,31 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Pair;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.utils.AsyncLoader; import com.android.settings.overlay.FeatureFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** /**
* AsyncTask to retrieve Settings, First party app and any intent based results. * AsyncTask to retrieve Settings, first party app and any intent based results.
*/ */
public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult>> { public class DatabaseResultLoader extends FutureTask<List<? extends SearchResult>> {
private static final String LOG = "DatabaseResultLoader";
/* These indices are used to match the columns of the this loader's SELECT statement. private static final String TAG = "DatabaseResultLoader";
These are not necessarily the same order nor similar coverage as the schema defined in
IndexDatabaseHelper */
public static final int COLUMN_INDEX_ID = 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_CLASS_NAME = 4;
public static final int COLUMN_INDEX_SCREEN_TITLE = 5;
public static final int COLUMN_INDEX_ICON = 6;
public static final int COLUMN_INDEX_INTENT_ACTION = 7;
public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
public static final int COLUMN_INDEX_KEY = 10;
public static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
public static final int COLUMN_INDEX_PAYLOAD = 12;
public static final String[] SELECT_COLUMNS = { public static final String[] SELECT_COLUMNS = {
IndexColumns.DOCID, IndexColumns.DOCID,
@@ -82,194 +78,267 @@ public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult
IndexColumns.DATA_SUMMARY_OFF_NORMALIZED, IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
}; };
public static final String[] MATCH_COLUMNS_TERTIARY = {
IndexColumns.DATA_KEYWORDS,
IndexColumns.DATA_ENTRIES
};
/** /**
* Base ranks defines the best possible rank based on what the query matches. * Base ranks defines the best possible rank based on what the query matches.
* If the query matches the prefix of the first word in the title, the best rank it can be is 1 * If the query matches the prefix of the first word in the title, the best rank it can be
* If the query matches the prefix of the other words in the title, the best rank it can be is 3 * is 1
* If the query matches the prefix of the other words in the title, the best rank it can be
* is 3
* If the query only matches the summary, the best rank it can be is 7 * If the query only matches the summary, the best rank it can be is 7
* If the query only matches keywords or entries, the best rank it can be is 9 * If the query only matches keywords or entries, the best rank it can be is 9
*/ */
public static final int[] BASE_RANKS = {1, 3, 7, 9}; public static final int[] BASE_RANKS = {1, 3, 7, 9};
@VisibleForTesting public DatabaseResultLoader(Context context, String query, SiteMapManager manager) {
final String mQueryText; super(new StaticSearchResultCallable(context, query, manager));
private final Context mContext;
private final CursorToSearchResultConverter mConverter;
private final SiteMapManager mSiteMapManager;
public DatabaseResultLoader(Context context, String queryText, SiteMapManager mapManager) {
super(context);
mSiteMapManager = mapManager;
mContext = context;
mQueryText = queryText;
mConverter = new CursorToSearchResultConverter(context);
} }
@Override static class StaticSearchResultCallable implements
protected void onDiscardResult(Set<? extends SearchResult> result) { Callable<List<? extends SearchResult>> {
// TODO Search
}
@Override public final String[] MATCH_COLUMNS_TERTIARY = {
public Set<? extends SearchResult> loadInBackground() { IndexColumns.DATA_KEYWORDS,
if (mQueryText == null || mQueryText.isEmpty()) { IndexColumns.DATA_ENTRIES
return null; };
@VisibleForTesting
final String mQueryText;
private final Context mContext;
private final CursorToSearchResultConverter mConverter;
private final SiteMapManager mSiteMapManager;
private final SearchFeatureProvider mFeatureProvider;
public StaticSearchResultCallable(Context context, String queryText,
SiteMapManager mapManager) {
mContext = context;
mSiteMapManager = mapManager;
mQueryText = queryText;
mConverter = new CursorToSearchResultConverter(context);
mFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
} }
final Set<SearchResult> results = new HashSet<>(); @Override
public List<? extends SearchResult> call() {
if (mQueryText == null || mQueryText.isEmpty()) {
return new ArrayList<>();
}
results.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0])); // TODO (b/68656233) Consolidate timing metrics
results.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1])); long startTime = System.currentTimeMillis();
results.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2])); // Start a Future to get search result scores.
results.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3])); FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
return results; mContext, mQueryText);
}
@Override if (rankerTask != null) {
protected boolean onCancelLoad() { ExecutorService executorService = mFeatureProvider.getExecutorService();
// TODO executorService.execute(rankerTask);
return super.onCancelLoad(); }
}
/** final Set<SearchResult> resultSet = new HashSet<>();
* Creates and executes the query which matches prefixes of the first word of the given columns.
*
* @param matchColumns The columns to match on
* @param baseRank The highest rank achievable by these results
* @return A set of the matching results.
*/
private Set<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
final String whereClause = buildSingleWordWhereClause(matchColumns);
final String query = mQueryText + "%";
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
return query(whereClause, selection, baseRank); resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
} resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
/** // Try to retrieve the scores in time. Otherwise use static ranking.
* Creates and executes the query which matches prefixes of the non-first words of the if (rankerTask != null) {
* given columns. try {
* final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext);
* @param matchColumns The columns to match on List<Pair<String, Float>> searchRankScores = rankerTask.get(timeoutMs,
* @param baseRank The highest rank achievable by these results TimeUnit.MILLISECONDS);
* @return A set of the matching results. return getDynamicRankedResults(resultSet, searchRankScores);
*/ } catch (TimeoutException | InterruptedException | ExecutionException e) {
private Set<SearchResult> secondaryWordQuery(String[] matchColumns, int baseRank) { Log.d(TAG, "Error waiting for result scores: " + e);
final String whereClause = buildSingleWordWhereClause(matchColumns); }
final String query = "% " + mQueryText + "%"; }
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
return query(whereClause, selection, baseRank); List<SearchResult> resultList = new ArrayList<>(resultSet);
} Collections.sort(resultList);
Log.i(TAG, "Static search loading took:" + (System.currentTimeMillis() - startTime));
/** return resultList;
* Creates and executes the query which matches prefixes of the any word of the given columns.
*
* @param matchColumns The columns to match on
* @param baseRank The highest rank achievable by these results
* @return A set of the matching results.
*/
private Set<SearchResult> anyWordQuery(String[] matchColumns, int baseRank) {
final String whereClause = buildTwoWordWhereClause(matchColumns);
final String[] selection = buildAnyWordSelection(matchColumns.length * 2);
return query(whereClause, selection, baseRank);
}
/**
* Generic method used by all of the query methods above to execute a query.
*
* @param whereClause Where clause for the SQL query which uses bindings.
* @param selection List of the transformed query to match each bind in the whereClause
* @param baseRank The highest rank achievable by these results.
* @return A set of the matching results.
*/
private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
final SQLiteDatabase database =
IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause,
selection, null, null, null)) {
return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank);
} }
}
/** // TODO (b/33577327) Retrieve all search results with a single query.
* Builds the SQLite WHERE clause that matches all matchColumns for a single query.
* /**
* @param matchColumns List of columns that will be used for matching. * Creates and executes the query which matches prefixes of the first word of the given
* @return The constructed WHERE clause. * columns.
*/ *
private static String buildSingleWordWhereClause(String[] matchColumns) { * @param matchColumns The columns to match on
StringBuilder sb = new StringBuilder(" ("); * @param baseRank The highest rank achievable by these results
final int count = matchColumns.length; * @return A set of the matching results.
for (int n = 0; n < count; n++) { */
sb.append(matchColumns[n]); private Set<SearchResult> firstWordQuery(String[] matchColumns, int baseRank) {
sb.append(" like ? "); final String whereClause = buildSingleWordWhereClause(matchColumns);
if (n < count - 1) { final String query = mQueryText + "%";
sb.append(" OR "); final String[] selection = buildSingleWordSelection(query, matchColumns.length);
return query(whereClause, selection, baseRank);
}
/**
* Creates and executes the query which matches prefixes of the non-first words of the
* given columns.
*
* @param matchColumns The columns to match on
* @param baseRank The highest rank achievable by these results
* @return A set of the matching results.
*/
private Set<SearchResult> secondaryWordQuery(String[] matchColumns, int baseRank) {
final String whereClause = buildSingleWordWhereClause(matchColumns);
final String query = "% " + mQueryText + "%";
final String[] selection = buildSingleWordSelection(query, matchColumns.length);
return query(whereClause, selection, baseRank);
}
/**
* Creates and executes the query which matches prefixes of the any word of the given
* columns.
*
* @param matchColumns The columns to match on
* @param baseRank The highest rank achievable by these results
* @return A set of the matching results.
*/
private Set<SearchResult> anyWordQuery(String[] matchColumns, int baseRank) {
final String whereClause = buildTwoWordWhereClause(matchColumns);
final String[] selection = buildAnyWordSelection(matchColumns.length * 2);
return query(whereClause, selection, baseRank);
}
/**
* Generic method used by all of the query methods above to execute a query.
*
* @param whereClause Where clause for the SQL query which uses bindings.
* @param selection List of the transformed query to match each bind in the whereClause
* @param baseRank The highest rank achievable by these results.
* @return A set of the matching results.
*/
private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
final SQLiteDatabase database =
IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
whereClause,
selection, null, null, null)) {
return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank);
} }
} }
sb.append(") AND enabled = 1");
return sb.toString();
}
/** /**
* Builds the SQLite WHERE clause that matches all matchColumns to two different queries. * Builds the SQLite WHERE clause that matches all matchColumns for a single query.
* *
* @param matchColumns List of columns that will be used for matching. * @param matchColumns List of columns that will be used for matching.
* @return The constructed WHERE clause. * @return The constructed WHERE clause.
*/ */
private static String buildTwoWordWhereClause(String[] matchColumns) { private static String buildSingleWordWhereClause(String[] matchColumns) {
StringBuilder sb = new StringBuilder(" ("); StringBuilder sb = new StringBuilder(" (");
final int count = matchColumns.length; final int count = matchColumns.length;
for (int n = 0; n < count; n++) { for (int n = 0; n < count; n++) {
sb.append(matchColumns[n]); sb.append(matchColumns[n]);
sb.append(" like ? OR "); sb.append(" like ? ");
sb.append(matchColumns[n]); if (n < count - 1) {
sb.append(" like ?"); sb.append(" OR ");
if (n < count - 1) { }
sb.append(" OR ");
} }
sb.append(") AND enabled = 1");
return sb.toString();
} }
sb.append(") AND enabled = 1");
return sb.toString();
}
/** /**
* Fills out the selection array to match the query as the prefix of a single word. * Builds the SQLite WHERE clause that matches all matchColumns to two different queries.
* *
* @param size is the number of columns to be matched. * @param matchColumns List of columns that will be used for matching.
*/ * @return The constructed WHERE clause.
private String[] buildSingleWordSelection(String query, int size) { */
String[] selection = new String[size]; private static String buildTwoWordWhereClause(String[] matchColumns) {
StringBuilder sb = new StringBuilder(" (");
for (int i = 0; i < size; i++) { final int count = matchColumns.length;
selection[i] = query; for (int n = 0; n < count; n++) {
sb.append(matchColumns[n]);
sb.append(" like ? OR ");
sb.append(matchColumns[n]);
sb.append(" like ?");
if (n < count - 1) {
sb.append(" OR ");
}
}
sb.append(") AND enabled = 1");
return sb.toString();
} }
return selection;
}
/** /**
* Fills out the selection array to match the query as the prefix of a word. * Fills out the selection array to match the query as the prefix of a single word.
* *
* @param size is twice the number of columns to be matched. The first match is for the prefix * @param size is the number of columns to be matched.
* of the first word in the column. The second match is for any subsequent word */
* prefix match. private String[] buildSingleWordSelection(String query, int size) {
*/ String[] selection = new String[size];
private String[] buildAnyWordSelection(int size) {
String[] selection = new String[size];
final String query = mQueryText + "%";
final String subStringQuery = "% " + mQueryText + "%";
for (int i = 0; i < (size - 1); i += 2) { for (int i = 0; i < size; i++) {
selection[i] = query; selection[i] = query;
selection[i + 1] = subStringQuery; }
return selection;
}
/**
* Fills out the selection array to match the query as the prefix of a word.
*
* @param size is twice the number of columns to be matched. The first match is for the
* prefix
* of the first word in the column. The second match is for any subsequent word
* prefix match.
*/
private String[] buildAnyWordSelection(int size) {
String[] selection = new String[size];
final String query = mQueryText + "%";
final String subStringQuery = "% " + mQueryText + "%";
for (int i = 0; i < (size - 1); i += 2) {
selection[i] = query;
selection[i + 1] = subStringQuery;
}
return selection;
}
private List<SearchResult> getDynamicRankedResults(Set<SearchResult> unsortedSet,
List<Pair<String, Float>> searchRankScores) {
TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
(o1, o2) -> {
float score1 = getRankingScoreByStableId(searchRankScores, o1.stableId);
float score2 = getRankingScoreByStableId(searchRankScores, o2.stableId);
if (score1 > score2) {
return -1;
} else if (score1 == score2) {
return 0;
} else {
return 1;
}
});
dbResultsSortedByScores.addAll(unsortedSet);
return new ArrayList<>(dbResultsSortedByScores);
}
/**
* Looks up ranking score for stableId
*
* @param stableId String of stableId
* @return the ranking score corresponding to the given stableId. If there is no score
* available for this stableId, -Float.MAX_VALUE is returned.
*/
@VisibleForTesting
Float getRankingScoreByStableId(List<Pair<String, Float>> searchRankScores, int stableId) {
for (Pair<String, Float> rankingScore : searchRankScores) {
if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) {
return rankingScore.second;
}
}
// If stableId not found in the list, we assign the minimum score so it will appear at
// the end of the list.
Log.w(TAG, "stableId " + stableId + " was not in the ranking scores.");
return -Float.MAX_VALUE;
} }
return selection;
} }
} }

View File

@@ -26,6 +26,7 @@ import android.content.pm.ServiceInfo;
import android.hardware.input.InputManager; import android.hardware.input.InputManager;
import android.hardware.input.KeyboardLayout; import android.hardware.input.KeyboardLayout;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
@@ -35,20 +36,24 @@ import com.android.settings.R;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment;
import com.android.settings.inputmethod.PhysicalKeyboardFragment; import com.android.settings.inputmethod.PhysicalKeyboardFragment;
import com.android.settings.utils.AsyncLoader;
import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil; import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/** /**
* Search result for input devices (physical/virtual keyboard, game controllers, etc) * Search result for input devices (physical/virtual keyboard, game controllers, etc)
*/ */
public class InputDeviceResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
private static final int NAME_NO_MATCH = -1; public class InputDeviceResultLoader extends FutureTask<List<? extends SearchResult>> {
private static final String TAG = "InputResultFutureTask";
@VisibleForTesting @VisibleForTesting
static final String PHYSICAL_KEYBOARD_FRAGMENT = PhysicalKeyboardFragment.class.getName(); static final String PHYSICAL_KEYBOARD_FRAGMENT = PhysicalKeyboardFragment.class.getName();
@@ -56,145 +61,151 @@ public class InputDeviceResultLoader extends AsyncLoader<Set<? extends SearchRes
static final String VIRTUAL_KEYBOARD_FRAGMENT = static final String VIRTUAL_KEYBOARD_FRAGMENT =
AvailableVirtualKeyboardFragment.class.getName(); AvailableVirtualKeyboardFragment.class.getName();
private final SiteMapManager mSiteMapManager; public InputDeviceResultLoader(Context context, String query, SiteMapManager manager) {
private final InputManager mInputManager; super(new InputDeviceResultCallable(context, query, manager));
private final InputMethodManager mImm;
private final PackageManager mPackageManager;
@VisibleForTesting
final String mQuery;
private List<String> mPhysicalKeyboardBreadcrumb;
private List<String> mVirtualKeyboardBreadcrumb;
public InputDeviceResultLoader(Context context, String query, SiteMapManager mapManager) {
super(context);
mQuery = query;
mSiteMapManager = mapManager;
mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
mImm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE);
mPackageManager = context.getPackageManager();
} }
@Override static class InputDeviceResultCallable implements
protected void onDiscardResult(Set<? extends SearchResult> result) { Callable<List<? extends SearchResult>> {
} private static final int NAME_NO_MATCH = -1;
@Override private final Context mContext;
public Set<? extends SearchResult> loadInBackground() { private final SiteMapManager mSiteMapManager;
final Set<SearchResult> results = new HashSet<>(); private final InputManager mInputManager;
results.addAll(buildPhysicalKeyboardSearchResults()); private final InputMethodManager mImm;
results.addAll(buildVirtualKeyboardSearchResults()); private final PackageManager mPackageManager;
return results; @VisibleForTesting
} final String mQuery;
private Set<SearchResult> buildPhysicalKeyboardSearchResults() { private List<String> mPhysicalKeyboardBreadcrumb;
final Set<SearchResult> results = new HashSet<>(); private List<String> mVirtualKeyboardBreadcrumb;
final Context context = getContext();
final String screenTitle = context.getString(R.string.physical_keyboard_title);
for (final InputDevice device : getPhysicalFullKeyboards()) { public InputDeviceResultCallable(Context context, String query, SiteMapManager mapManager) {
final String deviceName = device.getName(); mContext = context;
final int wordDiff = InstalledAppResultLoader.getWordDifference(deviceName, mQuery); mQuery = query;
if (wordDiff == NAME_NO_MATCH) { mSiteMapManager = mapManager;
continue; mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
mImm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE);
mPackageManager = context.getPackageManager();
}
@Override
public List<? extends SearchResult> call() {
long startTime = System.currentTimeMillis();
final List<SearchResult> results = new ArrayList<>();
results.addAll(buildPhysicalKeyboardSearchResults());
results.addAll(buildVirtualKeyboardSearchResults());
Collections.sort(results);
Log.i(TAG, "Input search loading took:" + (System.currentTimeMillis() - startTime));
return results;
}
private Set<SearchResult> buildPhysicalKeyboardSearchResults() {
final Set<SearchResult> results = new HashSet<>();
final String screenTitle = mContext.getString(R.string.physical_keyboard_title);
for (final InputDevice device : getPhysicalFullKeyboards()) {
final String deviceName = device.getName();
final int wordDiff = InstalledAppResultLoader.getWordDifference(deviceName,
mQuery);
if (wordDiff == NAME_NO_MATCH) {
continue;
}
final String keyboardLayoutDescriptor = mInputManager
.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
final KeyboardLayout keyboardLayout = (keyboardLayoutDescriptor != null)
? mInputManager.getKeyboardLayout(keyboardLayoutDescriptor) : null;
final String summary = (keyboardLayout != null)
? keyboardLayout.toString()
: mContext.getString(R.string.keyboard_layout_default_label);
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext,
PHYSICAL_KEYBOARD_FRAGMENT, deviceName, screenTitle);
results.add(new SearchResult.Builder()
.setTitle(deviceName)
.setPayload(new ResultPayload(intent))
.setStableId(Objects.hash(PHYSICAL_KEYBOARD_FRAGMENT, deviceName))
.setSummary(summary)
.setRank(wordDiff)
.addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
.build());
} }
final String keyboardLayoutDescriptor = mInputManager return results;
.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
final KeyboardLayout keyboardLayout = (keyboardLayoutDescriptor != null)
? mInputManager.getKeyboardLayout(keyboardLayoutDescriptor) : null;
final String summary = (keyboardLayout != null)
? keyboardLayout.toString()
: context.getString(R.string.keyboard_layout_default_label);
final String key = deviceName;
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
PHYSICAL_KEYBOARD_FRAGMENT, key, screenTitle);
results.add(new SearchResult.Builder()
.setTitle(deviceName)
.setPayload(new ResultPayload(intent))
.setStableId(Objects.hash(PHYSICAL_KEYBOARD_FRAGMENT, key))
.setSummary(summary)
.setRank(wordDiff)
.addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
.build());
} }
return results;
}
private Set<SearchResult> buildVirtualKeyboardSearchResults() { private Set<SearchResult> buildVirtualKeyboardSearchResults() {
final Set<SearchResult> results = new HashSet<>(); final Set<SearchResult> results = new HashSet<>();
final Context context = getContext(); final String screenTitle = mContext.getString(R.string.add_virtual_keyboard);
final String screenTitle = context.getString(R.string.add_virtual_keyboard); final List<InputMethodInfo> inputMethods = mImm.getInputMethodList();
final List<InputMethodInfo> inputMethods = mImm.getInputMethodList(); for (InputMethodInfo info : inputMethods) {
for (InputMethodInfo info : inputMethods) { final String title = info.loadLabel(mPackageManager).toString();
final String title = info.loadLabel(mPackageManager).toString(); final String summary = InputMethodAndSubtypeUtil
final String summary = InputMethodAndSubtypeUtil .getSubtypeLocaleNameListAsSentence(getAllSubtypesOf(info), mContext, info);
.getSubtypeLocaleNameListAsSentence(getAllSubtypesOf(info), context, info); int wordDiff = InstalledAppResultLoader.getWordDifference(title, mQuery);
int wordDiff = InstalledAppResultLoader.getWordDifference(title, mQuery); if (wordDiff == NAME_NO_MATCH) {
if (wordDiff == NAME_NO_MATCH) { wordDiff = InstalledAppResultLoader.getWordDifference(summary, mQuery);
wordDiff = InstalledAppResultLoader.getWordDifference(summary, mQuery); }
if (wordDiff == NAME_NO_MATCH) {
continue;
}
final ServiceInfo serviceInfo = info.getServiceInfo();
final String key = new ComponentName(serviceInfo.packageName, serviceInfo.name)
.flattenToString();
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext,
VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
results.add(new SearchResult.Builder()
.setTitle(title)
.setSummary(summary)
.setRank(wordDiff)
.setStableId(Objects.hash(VIRTUAL_KEYBOARD_FRAGMENT, key))
.addBreadcrumbs(getVirtualKeyboardBreadCrumb())
.setPayload(new ResultPayload(intent))
.build());
} }
if (wordDiff == NAME_NO_MATCH) { return results;
continue; }
private List<String> getPhysicalKeyboardBreadCrumb() {
if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) {
mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
mContext, PHYSICAL_KEYBOARD_FRAGMENT,
mContext.getString(R.string.physical_keyboard_title));
} }
final ServiceInfo serviceInfo = info.getServiceInfo(); return mPhysicalKeyboardBreadcrumb;
final String key = new ComponentName(serviceInfo.packageName, serviceInfo.name)
.flattenToString();
final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
results.add(new SearchResult.Builder()
.setTitle(title)
.setSummary(summary)
.setRank(wordDiff)
.setStableId(Objects.hash(VIRTUAL_KEYBOARD_FRAGMENT, key))
.addBreadcrumbs(getVirtualKeyboardBreadCrumb())
.setPayload(new ResultPayload(intent))
.build());
} }
return results;
}
private List<String> getPhysicalKeyboardBreadCrumb() {
if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) { private List<String> getVirtualKeyboardBreadCrumb() {
final Context context = getContext(); if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) {
mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb( final Context context = mContext;
context, PHYSICAL_KEYBOARD_FRAGMENT, mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
context.getString(R.string.physical_keyboard_title)); context, VIRTUAL_KEYBOARD_FRAGMENT,
context.getString(R.string.add_virtual_keyboard));
}
return mVirtualKeyboardBreadcrumb;
} }
return mPhysicalKeyboardBreadcrumb;
}
private List<InputDevice> getPhysicalFullKeyboards() {
private List<String> getVirtualKeyboardBreadCrumb() { final List<InputDevice> keyboards = new ArrayList<>();
if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) { final int[] deviceIds = InputDevice.getDeviceIds();
final Context context = getContext(); if (deviceIds != null) {
mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb( for (int deviceId : deviceIds) {
context, VIRTUAL_KEYBOARD_FRAGMENT, final InputDevice device = InputDevice.getDevice(deviceId);
context.getString(R.string.add_virtual_keyboard)); if (device != null && !device.isVirtual() && device.isFullKeyboard()) {
} keyboards.add(device);
return mVirtualKeyboardBreadcrumb; }
}
private List<InputDevice> getPhysicalFullKeyboards() {
final List<InputDevice> keyboards = new ArrayList<>();
final int[] deviceIds = InputDevice.getDeviceIds();
if (deviceIds != null) {
for (int deviceId : deviceIds) {
final InputDevice device = InputDevice.getDevice(deviceId);
if (device != null && !device.isVirtual() && device.isFullKeyboard()) {
keyboards.add(device);
} }
} }
return keyboards;
} }
return keyboards;
}
private static List<InputMethodSubtype> getAllSubtypesOf(final InputMethodInfo imi) { private static List<InputMethodSubtype> getAllSubtypesOf(final InputMethodInfo imi) {
final int subtypeCount = imi.getSubtypeCount(); final int subtypeCount = imi.getSubtypeCount();
final List<InputMethodSubtype> allSubtypes = new ArrayList<>(subtypeCount); final List<InputMethodSubtype> allSubtypes = new ArrayList<>(subtypeCount);
for (int index = 0; index < subtypeCount; index++) { for (int index = 0; index < subtypeCount; index++) {
allSubtypes.add(imi.getSubtypeAt(index)); allSubtypes.add(imi.getSubtypeAt(index));
}
return allSubtypes;
} }
return allSubtypes;
} }
} }

View File

@@ -29,124 +29,39 @@ import android.os.UserManager;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import com.android.internal.logging.nano.MetricsProto; import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.SettingsActivity; import com.android.settings.SettingsActivity;
import com.android.settings.applications.manageapplications.ManageApplications; import com.android.settings.applications.manageapplications.ManageApplications;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.utils.AsyncLoader;
import com.android.settingslib.wrapper.PackageManagerWrapper; import com.android.settingslib.wrapper.PackageManagerWrapper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
/** /**
* Search loader for installed apps. * Search loader for installed apps.
*/ */
public class InstalledAppResultLoader extends AsyncLoader<Set<? extends SearchResult>> { public class InstalledAppResultLoader extends FutureTask<List<? extends SearchResult>> {
private static final String TAG = "InstalledAppFutureTask";
private static final int NAME_NO_MATCH = -1; private static final int NAME_NO_MATCH = -1;
private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN) private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_LAUNCHER); .addCategory(Intent.CATEGORY_LAUNCHER);
private List<String> mBreadcrumb; public InstalledAppResultLoader(Context context, PackageManagerWrapper wrapper,
private SiteMapManager mSiteMapManager; String query, SiteMapManager manager) {
@VisibleForTesting super(new InstalledAppResultCallable(context, wrapper, query, manager));
final String mQuery;
private final UserManager mUserManager;
private final PackageManagerWrapper mPackageManager;
private final List<ResolveInfo> mHomeActivities = new ArrayList<>();
public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper,
String query, SiteMapManager mapManager) {
super(context);
mSiteMapManager = mapManager;
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mPackageManager = pmWrapper;
mQuery = query;
}
@Override
public Set<? extends SearchResult> loadInBackground() {
final Set<AppSearchResult> results = new HashSet<>();
final PackageManager pm = mPackageManager.getPackageManager();
mHomeActivities.clear();
mPackageManager.getHomeActivities(mHomeActivities);
for (UserInfo user : getUsersToCount()) {
final List<ApplicationInfo> apps =
mPackageManager.getInstalledApplicationsAsUser(
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
| (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0),
user.id);
for (ApplicationInfo info : apps) {
if (!shouldIncludeAsCandidate(info, user)) {
continue;
}
final CharSequence label = info.loadLabel(pm);
final int wordDiff = getWordDifference(label.toString(), mQuery);
if (wordDiff == NAME_NO_MATCH) {
continue;
}
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", info.packageName, null))
.putExtra(SettingsActivity.EXTRA_SOURCE_METRICS_CATEGORY,
MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS);
final AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setAppInfo(info)
.setStableId(Objects.hash(info.packageName, user.id))
.setTitle(info.loadLabel(pm))
.setRank(getRank(wordDiff))
.addBreadcrumbs(getBreadCrumb())
.setPayload(new ResultPayload(intent));
results.add(builder.build());
}
}
return results;
}
/**
* Returns true if the candidate should be included in candidate list
* <p/>
* This method matches logic in {@code ApplicationState#FILTER_DOWNLOADED_AND_LAUNCHER}.
*/
private boolean shouldIncludeAsCandidate(ApplicationInfo info, UserInfo user) {
// Not system app
if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
|| (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
return true;
}
// Shows up in launcher
final Intent launchIntent = new Intent(LAUNCHER_PROBE)
.setPackage(info.packageName);
final List<ResolveInfo> intents = mPackageManager.queryIntentActivitiesAsUser(
launchIntent,
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
user.id);
if (intents != null && intents.size() != 0) {
return true;
}
// Is launcher app itself
return isPackageInList(mHomeActivities, info.packageName);
}
@Override
protected void onDiscardResult(Set<? extends SearchResult> result) {
}
private List<UserInfo> getUsersToCount() {
return mUserManager.getProfiles(UserHandle.myUserId());
} }
/** /**
@@ -213,35 +128,133 @@ public class InstalledAppResultLoader extends AsyncLoader<Set<? extends SearchRe
return NAME_NO_MATCH; return NAME_NO_MATCH;
} }
private boolean isPackageInList(List<ResolveInfo> resolveInfos, String pkg) { static class InstalledAppResultCallable implements
for (ResolveInfo info : resolveInfos) { Callable<List<? extends SearchResult>> {
if (TextUtils.equals(info.activityInfo.packageName, pkg)) {
private final Context mContext;
private List<String> mBreadcrumb;
private SiteMapManager mSiteMapManager;
@VisibleForTesting
final String mQuery;
private final UserManager mUserManager;
private final PackageManagerWrapper mPackageManager;
private final List<ResolveInfo> mHomeActivities = new ArrayList<>();
public InstalledAppResultCallable(Context context, PackageManagerWrapper pmWrapper,
String query, SiteMapManager mapManager) {
mContext = context;
mSiteMapManager = mapManager;
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mPackageManager = pmWrapper;
mQuery = query;
}
@Override
public List<? extends SearchResult> call() throws Exception {
long startTime = System.currentTimeMillis();
final List<AppSearchResult> results = new ArrayList<>();
final PackageManager pm = mPackageManager.getPackageManager();
mHomeActivities.clear();
mPackageManager.getHomeActivities(mHomeActivities);
for (UserInfo user : getUsersToCount()) {
final List<ApplicationInfo> apps =
mPackageManager.getInstalledApplicationsAsUser(
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
| (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0),
user.id);
for (ApplicationInfo info : apps) {
if (!shouldIncludeAsCandidate(info, user)) {
continue;
}
final CharSequence label = info.loadLabel(pm);
final int wordDiff = getWordDifference(label.toString(), mQuery);
if (wordDiff == NAME_NO_MATCH) {
continue;
}
final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", info.packageName, null))
.putExtra(SettingsActivity.EXTRA_SOURCE_METRICS_CATEGORY,
MetricsProto.MetricsEvent.DASHBOARD_SEARCH_RESULTS);
final AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setAppInfo(info)
.setStableId(Objects.hash(info.packageName, user.id))
.setTitle(info.loadLabel(pm))
.setRank(getRank(wordDiff))
.addBreadcrumbs(getBreadCrumb())
.setPayload(new ResultPayload(intent));
results.add(builder.build());
}
}
Collections.sort(results);
Log.i(TAG, "App search loading took:" + (System.currentTimeMillis() - startTime));
return results;
}
/**
* Returns true if the candidate should be included in candidate list
* <p/>
* This method matches logic in {@code ApplicationState#FILTER_DOWNLOADED_AND_LAUNCHER}.
*/
private boolean shouldIncludeAsCandidate(ApplicationInfo info, UserInfo user) {
// Not system app
if ((info.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0
|| (info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
return true; return true;
} }
// Shows up in launcher
final Intent launchIntent = new Intent(LAUNCHER_PROBE)
.setPackage(info.packageName);
final List<ResolveInfo> intents = mPackageManager.queryIntentActivitiesAsUser(
launchIntent,
PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
user.id);
if (intents != null && intents.size() != 0) {
return true;
}
// Is launcher app itself
return isPackageInList(mHomeActivities, info.packageName);
} }
return false;
}
private List<String> getBreadCrumb() { private List<UserInfo> getUsersToCount() {
if (mBreadcrumb == null || mBreadcrumb.isEmpty()) { return mUserManager.getProfiles(UserHandle.myUserId());
final Context context = getContext();
mBreadcrumb = mSiteMapManager.buildBreadCrumb(
context, ManageApplications.class.getName(),
context.getString(R.string.applications_settings));
} }
return mBreadcrumb;
}
/** private boolean isPackageInList(List<ResolveInfo> resolveInfos, String pkg) {
* A temporary ranking scheme for installed apps. for (ResolveInfo info : resolveInfos) {
* if (TextUtils.equals(info.activityInfo.packageName, pkg)) {
* @param wordDiff difference between query length and app name length. return true;
* @return the ranking. }
*/ }
private int getRank(int wordDiff) { return false;
if (wordDiff < 6) { }
return 2;
private List<String> getBreadCrumb() {
if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
mBreadcrumb = mSiteMapManager.buildBreadCrumb(
mContext, ManageApplications.class.getName(),
mContext.getString(R.string.applications_settings));
}
return mBreadcrumb;
}
/**
* A temporary ranking scheme for installed apps.
*
* @param wordDiff difference between query length and app name length.
* @return the ranking.
*/
private int getRank(int wordDiff) {
if (wordDiff < 6) {
return 2;
}
return 3;
} }
return 3;
} }
} }

View File

@@ -19,10 +19,14 @@ package com.android.settings.search;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.util.Pair;
import android.view.View; import android.view.View;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.search.ranking.SearchResultsRankerCallback;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
/** /**
* FeatureProvider for Settings Search * FeatureProvider for Settings Search
@@ -43,26 +47,31 @@ public interface SearchFeatureProvider {
void verifyLaunchSearchResultPageCaller(Context context, @NonNull ComponentName caller) void verifyLaunchSearchResultPageCaller(Context context, @NonNull ComponentName caller)
throws SecurityException, IllegalArgumentException; throws SecurityException, IllegalArgumentException;
/**
* Returns a new loader to get settings search results.
*/
SearchResultLoader getSearchResultLoader(Context context, String query);
/** /**
* Returns a new loader to search in index database. * Returns a new loader to search in index database.
*/ */
DatabaseResultLoader getDatabaseSearchLoader(Context context, String query); DatabaseResultLoader getStaticSearchResultTask(Context context, String query);
/** /**
* Returns a new loader to search installed apps. * Returns a new loader to search installed apps.
*/ */
InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query); InstalledAppResultLoader getInstalledAppSearchTask(Context context, String query);
/** /**
* Returns a new loader to search accessibility services. * Returns a new loader to search accessibility services.
*/ */
AccessibilityServiceResultLoader getAccessibilityServiceResultLoader(Context context, AccessibilityServiceResultLoader getAccessibilityServiceResultTask(Context context,
String query); String query);
/** /**
* Returns a new loader to search input devices. * Returns a new loader to search input devices.
*/ */
InputDeviceResultLoader getInputDeviceResultLoader(Context context, String query); InputDeviceResultLoader getInputDeviceResultTask(Context context, String query);
/** /**
* Returns a new loader to get all recently saved queries search terms. * Returns a new loader to get all recently saved queries search terms.
@@ -95,6 +104,11 @@ public interface SearchFeatureProvider {
*/ */
boolean isIndexingComplete(Context context); boolean isIndexingComplete(Context context);
/**
* @return a {@link ExecutorService} to be shared between search tasks.
*/
ExecutorService getExecutorService();
/** /**
* Initializes the feedback button in case it was dismissed. * Initializes the feedback button in case it was dismissed.
*/ */
@@ -114,23 +128,6 @@ public interface SearchFeatureProvider {
default void hideFeedbackButton() { default void hideFeedbackButton() {
} }
/**
* Query search results based on the input query.
*
* @param context application context
* @param query input user query
* @param searchResultsRankerCallback {@link SearchResultsRankerCallback}
*/
default void querySearchResults(Context context, String query,
SearchResultsRankerCallback searchResultsRankerCallback) {
}
/**
* Cancel pending search query
*/
default void cancelPendingSearchQuery(Context context) {
}
/** /**
* Notify that a search result is clicked. * Notify that a search result is clicked.
* *
@@ -161,4 +158,10 @@ public interface SearchFeatureProvider {
default void searchRankingWarmup(Context context) { default void searchRankingWarmup(Context context) {
} }
/**
* Return a FutureTask to get a list of scores for search results.
*/
default FutureTask<List<Pair<String, Float>>> getRankerTask(Context context, String query) {
return null;
}
} }

View File

@@ -22,12 +22,15 @@ import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.FeatureFactory;
import com.android.settings.search.indexing.IndexData; import com.android.settings.search.indexing.IndexData;
import com.android.settingslib.wrapper.PackageManagerWrapper; import com.android.settingslib.wrapper.PackageManagerWrapper;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** /**
* FeatureProvider for the refactored search code. * FeatureProvider for the refactored search code.
@@ -40,6 +43,7 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
private DatabaseIndexingManager mDatabaseIndexingManager; private DatabaseIndexingManager mDatabaseIndexingManager;
private SiteMapManager mSiteMapManager; private SiteMapManager mSiteMapManager;
private ExecutorService mExecutorService;
@Override @Override
public boolean isEnabled(Context context) { public boolean isEnabled(Context context) {
@@ -59,26 +63,31 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
} }
@Override @Override
public DatabaseResultLoader getDatabaseSearchLoader(Context context, String query) { public SearchResultLoader getSearchResultLoader(Context context, String query) {
return new SearchResultLoader(context, cleanQuery(query));
}
@Override
public DatabaseResultLoader getStaticSearchResultTask(Context context, String query) {
return new DatabaseResultLoader(context, cleanQuery(query), getSiteMapManager()); return new DatabaseResultLoader(context, cleanQuery(query), getSiteMapManager());
} }
@Override @Override
public InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query) { public InstalledAppResultLoader getInstalledAppSearchTask(Context context, String query) {
return new InstalledAppResultLoader( return new InstalledAppResultLoader(
context, new PackageManagerWrapper(context.getPackageManager()), context, new PackageManagerWrapper(context.getPackageManager()),
cleanQuery(query), getSiteMapManager()); cleanQuery(query), getSiteMapManager());
} }
@Override @Override
public AccessibilityServiceResultLoader getAccessibilityServiceResultLoader(Context context, public AccessibilityServiceResultLoader getAccessibilityServiceResultTask(Context context,
String query) { String query) {
return new AccessibilityServiceResultLoader(context, cleanQuery(query), return new AccessibilityServiceResultLoader(context, cleanQuery(query),
getSiteMapManager()); getSiteMapManager());
} }
@Override @Override
public InputDeviceResultLoader getInputDeviceResultLoader(Context context, String query) { public InputDeviceResultLoader getInputDeviceResultTask(Context context, String query) {
return new InputDeviceResultLoader(context, cleanQuery(query), getSiteMapManager()); return new InputDeviceResultLoader(context, cleanQuery(query), getSiteMapManager());
} }
@@ -124,12 +133,21 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
.histogram(context, METRICS_ACTION_SETTINGS_INDEX, indexingTime); .histogram(context, METRICS_ACTION_SETTINGS_INDEX, indexingTime);
} }
@Override
public ExecutorService getExecutorService() {
if (mExecutorService == null) {
mExecutorService = Executors.newCachedThreadPool();
}
return mExecutorService;
}
/** /**
* A generic method to make the query suitable for searching the database. * A generic method to make the query suitable for searching the database.
* *
* @return the cleaned query string * @return the cleaned query string
*/ */
private String cleanQuery(String query) { @VisibleForTesting
String cleanQuery(String query) {
if (TextUtils.isEmpty(query)) { if (TextUtils.isEmpty(query)) {
return null; return null;
} }

View File

@@ -54,8 +54,6 @@ import com.android.settings.widget.ActionBarShadowController;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/** /**
* This fragment manages the lifecycle of indexing and searching. * This fragment manages the lifecycle of indexing and searching.
@@ -68,7 +66,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* the query if the user has entered text. * the query if the user has entered text.
*/ */
public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener, public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener,
LoaderManager.LoaderCallbacks<Set<? extends SearchResult>>, IndexingCallback { LoaderManager.LoaderCallbacks<List<? extends SearchResult>>, IndexingCallback {
private static final String TAG = "SearchFragment"; private static final String TAG = "SearchFragment";
// State values // State values
@@ -78,23 +76,14 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
static final class SearchLoaderId { static final class SearchLoaderId {
// Search Query IDs // Search Query IDs
public static final int DATABASE = 1; public static final int SEARCH_RESULT = 1;
public static final int INSTALLED_APPS = 2;
public static final int ACCESSIBILITY_SERVICES = 3;
public static final int INPUT_DEVICES = 4;
// Saved Query IDs // Saved Query IDs
public static final int SAVE_QUERY_TASK = 5; public static final int SAVE_QUERY_TASK = 2;
public static final int REMOVE_QUERY_TASK = 6; public static final int REMOVE_QUERY_TASK = 3;
public static final int SAVED_QUERIES = 7; public static final int SAVED_QUERIES = 4;
} }
private static final int NUM_QUERY_LOADERS = 4;
@VisibleForTesting
AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);
@VisibleForTesting @VisibleForTesting
String mQuery; String mQuery;
@@ -147,7 +136,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
setHasOptionsMenu(true); setHasOptionsMenu(true);
final LoaderManager loaderManager = getLoaderManager(); final LoaderManager loaderManager = getLoaderManager();
mSearchAdapter = new SearchResultsAdapter(this, mSearchFeatureProvider); mSearchAdapter = new SearchResultsAdapter(this /* fragment */);
mSavedQueryController = new SavedQueryController( mSavedQueryController = new SavedQueryController(
getContext(), loaderManager, mSearchAdapter); getContext(), loaderManager, mSearchAdapter);
mSearchFeatureProvider.initFeedbackButton(); mSearchFeatureProvider.initFeedbackButton();
@@ -277,15 +266,11 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
if (isEmptyQuery) { if (isEmptyQuery) {
final LoaderManager loaderManager = getLoaderManager(); final LoaderManager loaderManager = getLoaderManager();
loaderManager.destroyLoader(SearchLoaderId.DATABASE); loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT);
loaderManager.destroyLoader(SearchLoaderId.INSTALLED_APPS);
loaderManager.destroyLoader(SearchLoaderId.ACCESSIBILITY_SERVICES);
loaderManager.destroyLoader(SearchLoaderId.INPUT_DEVICES);
mShowingSavedQuery = true; mShowingSavedQuery = true;
mSavedQueryController.loadSavedQueries(); mSavedQueryController.loadSavedQueries();
mSearchFeatureProvider.hideFeedbackButton(); mSearchFeatureProvider.hideFeedbackButton();
} else { } else {
mSearchAdapter.initializeSearch(mQuery);
restartLoaders(); restartLoaders();
} }
@@ -301,35 +286,25 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
} }
@Override @Override
public Loader<Set<? extends SearchResult>> onCreateLoader(int id, Bundle args) { public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
final Activity activity = getActivity(); final Activity activity = getActivity();
switch (id) { switch(id) {
case SearchLoaderId.DATABASE: case SearchLoaderId.SEARCH_RESULT:
return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery); return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery);
case SearchLoaderId.INSTALLED_APPS:
return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
case SearchLoaderId.ACCESSIBILITY_SERVICES:
return mSearchFeatureProvider.getAccessibilityServiceResultLoader(activity, mQuery);
case SearchLoaderId.INPUT_DEVICES:
return mSearchFeatureProvider.getInputDeviceResultLoader(activity, mQuery);
default: default:
return null; return null;
} }
} }
@Override @Override
public void onLoadFinished(Loader<Set<? extends SearchResult>> loader, public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
Set<? extends SearchResult> data) { List<? extends SearchResult> data) {
mSearchAdapter.addSearchResults(data, loader.getClass().getName()); mSearchAdapter.postSearchResults(data);
if (mUnfinishedLoadersCount.decrementAndGet() != 0) {
return;
}
mSearchAdapter.notifyResultsLoaded();
} }
@Override @Override
public void onLoaderReset(Loader<Set<? extends SearchResult>> loader) { public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
} }
/** /**
@@ -344,13 +319,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
mSavedQueryController.loadSavedQueries(); mSavedQueryController.loadSavedQueries();
} else { } else {
final LoaderManager loaderManager = getLoaderManager(); final LoaderManager loaderManager = getLoaderManager();
loaderManager.initLoader(SearchLoaderId.DATABASE, null /* args */, this /* callback */); loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */,
loaderManager.initLoader( this /* callback */);
SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
loaderManager.initLoader(
SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
loaderManager.initLoader(
SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
} }
requery(); requery();
@@ -388,15 +358,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
private void restartLoaders() { private void restartLoaders() {
mShowingSavedQuery = false; mShowingSavedQuery = false;
final LoaderManager loaderManager = getLoaderManager(); final LoaderManager loaderManager = getLoaderManager();
mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
loaderManager.restartLoader( loaderManager.restartLoader(
SearchLoaderId.DATABASE, null /* args */, this /* callback */); SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */);
loaderManager.restartLoader(
SearchLoaderId.INSTALLED_APPS, null /* args */, this /* callback */);
loaderManager.restartLoader(
SearchLoaderId.ACCESSIBILITY_SERVICES, null /* args */, this /* callback */);
loaderManager.restartLoader(
SearchLoaderId.INPUT_DEVICES, null /* args */, this /* callback */);
} }
public String getQuery() { public String getQuery() {
@@ -453,9 +416,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
taggedData.add(Pair.create( taggedData.add(Pair.create(
MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK, MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK,
resultViewHolder.getAdapterPosition())); resultViewHolder.getAdapterPosition()));
taggedData.add(Pair.create( // TODO (b/67744820) Move metrics to SettingsIntelligence (including ranking state).
MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE,
mSearchAdapter.getAsyncRankingState()));
taggedData.add(Pair.create( taggedData.add(Pair.create(
MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH, MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH,
TextUtils.isEmpty(mQuery) ? 0 : mQuery.length())); TextUtils.isEmpty(mQuery) ? 0 : mQuery.length()));

View File

@@ -0,0 +1,177 @@
package com.android.settings.search;
import android.annotation.NonNull;
import android.content.Context;
import android.util.Log;
import android.util.SparseArray;
import com.android.settings.overlay.FeatureFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Collects the sorted list of all setting search results.
*
* TODO (b/64939692) Convert the timing logs to metrics
*/
public class SearchResultAggregator {
private static final String TAG = "SearchResultAggregator";
/**
* Timeout for first task. Allows for longer delay.
*/
private static final long LONG_CHECK_TASK_TIMEOUT_MS = 500;
/**
* Timeout for subsequent tasks to allow for fast returning tasks.
*/
private static final long SHORT_CHECK_TASK_TIMEOUT_MS = 150;
private static SearchResultAggregator sResultAggregator;
// TODO (b/33577327) Merge the other loaders into a single dynamic loader
static final class ResultLoaderId {
static final int STATIC_RESULTS = 1;
static final int INSTALLED_RESULTS = 2;
static final int INPUT_RESULTS = 3;
static final int ACCESSIBILITY_RESULTS = 4;
}
private SearchResultAggregator() {
}
public static SearchResultAggregator getInstance() {
if (sResultAggregator == null) {
sResultAggregator = new SearchResultAggregator();
}
return sResultAggregator;
}
@NonNull
public synchronized List<? extends SearchResult> fetchResults(Context context, String query) {
SearchFeatureProvider mFeatureProvider = FeatureFactory.getFactory(
context).getSearchFeatureProvider();
ExecutorService executorService = mFeatureProvider.getExecutorService();
final DatabaseResultLoader staticResultsTask =
mFeatureProvider.getStaticSearchResultTask(context, query);
final InstalledAppResultLoader installedAppTask =
mFeatureProvider.getInstalledAppSearchTask(context, query);
final InputDeviceResultLoader inputDevicesTask =
mFeatureProvider.getInputDeviceResultTask(context, query);
final AccessibilityServiceResultLoader accessibilityServicesTask =
mFeatureProvider.getAccessibilityServiceResultTask(context,
query);
executorService.execute(staticResultsTask);
executorService.execute(installedAppTask);
executorService.execute(inputDevicesTask);
executorService.execute(accessibilityServicesTask);
SparseArray<List<? extends SearchResult>> resultsArray = new SparseArray<>();
List<? extends SearchResult> EMPTY_LIST = new ArrayList<>();
long allTasksStart = System.currentTimeMillis();
try {
resultsArray.put(ResultLoaderId.INPUT_RESULTS,
inputDevicesTask.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.d(TAG, "Could not retrieve input devices results in time: " + e);
resultsArray.put(ResultLoaderId.INPUT_RESULTS, EMPTY_LIST);
}
try {
resultsArray.put(ResultLoaderId.ACCESSIBILITY_RESULTS,
accessibilityServicesTask.get(SHORT_CHECK_TASK_TIMEOUT_MS,
TimeUnit.MILLISECONDS));
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.d(TAG, "Could not retrieve accessibility results in time: " + e);
resultsArray.put(ResultLoaderId.ACCESSIBILITY_RESULTS, EMPTY_LIST);
}
try {
resultsArray.put(ResultLoaderId.STATIC_RESULTS,
staticResultsTask.get(LONG_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.d(TAG, "Could not retrieve static results: " + e);
resultsArray.put(ResultLoaderId.STATIC_RESULTS, EMPTY_LIST);
}
try {
resultsArray.put(ResultLoaderId.INSTALLED_RESULTS,
installedAppTask.get(SHORT_CHECK_TASK_TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.d(TAG, "Could not retrieve installed app results in time: " + e);
resultsArray.put(ResultLoaderId.INSTALLED_RESULTS, EMPTY_LIST);
}
long mergeStartTime = System.currentTimeMillis();
Log.i(TAG, "Total result loader time: " + (mergeStartTime - allTasksStart));
List<? extends SearchResult> mergedResults = mergeSearchResults(resultsArray);
Log.i(TAG, "Total merge time: " + (System.currentTimeMillis() - mergeStartTime));
Log.i(TAG, "Total aggregator time: " + (System.currentTimeMillis() - allTasksStart));
return mergedResults;
}
// TODO (b/68255021) scale the dynamic search results ranks and do a k-way merge
private List<? extends SearchResult> mergeSearchResults(
SparseArray<List<? extends SearchResult>> resultsArray) {
List<? extends SearchResult> staticResults = resultsArray.get(
ResultLoaderId.STATIC_RESULTS);
List<? extends SearchResult> installedAppResults = resultsArray.get(
ResultLoaderId.INSTALLED_RESULTS);
List<? extends SearchResult> accessibilityResults = resultsArray.get(
ResultLoaderId.ACCESSIBILITY_RESULTS);
List<? extends SearchResult> inputDeviceResults = resultsArray.get(
ResultLoaderId.INPUT_RESULTS);
List<SearchResult> searchResults;
int staticSize = staticResults.size();
int appSize = installedAppResults.size();
int a11ySize = accessibilityResults.size();
int inputDeviceSize = inputDeviceResults.size();
int appIndex = 0;
int a11yIndex = 0;
int inputDeviceIndex = 0;
int rank = SearchResult.TOP_RANK;
// TODO: We need a helper method to do k-way merge.
searchResults = new ArrayList<>(staticSize + appSize + a11ySize + inputDeviceSize);
searchResults.addAll(resultsArray.get(ResultLoaderId.STATIC_RESULTS));
while (rank <= SearchResult.BOTTOM_RANK) {
while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
searchResults.add(installedAppResults.get(appIndex++));
}
while ((a11yIndex < a11ySize) && (accessibilityResults.get(a11yIndex).rank == rank)) {
searchResults.add(accessibilityResults.get(a11yIndex++));
}
while (inputDeviceIndex < inputDeviceSize
&& inputDeviceResults.get(inputDeviceIndex).rank == rank) {
searchResults.add(inputDeviceResults.get(inputDeviceIndex++));
}
rank++;
}
while (appIndex < appSize) {
searchResults.add(installedAppResults.get(appIndex++));
}
while (a11yIndex < a11ySize) {
searchResults.add(accessibilityResults.get(a11yIndex++));
}
while (inputDeviceIndex < inputDeviceSize) {
searchResults.add(inputDeviceResults.get(inputDeviceIndex++));
}
return searchResults;
}
}

View File

@@ -26,10 +26,11 @@ import java.util.List;
*/ */
public class SearchResultDiffCallback extends DiffUtil.Callback { public class SearchResultDiffCallback extends DiffUtil.Callback {
private List<SearchResult> mOldList; private List<? extends SearchResult> mOldList;
private List<SearchResult> mNewList; private List<? extends SearchResult> mNewList;
public SearchResultDiffCallback(List<SearchResult> oldList, List<SearchResult> newList) { public SearchResultDiffCallback(List<? extends SearchResult> oldList,
List<? extends SearchResult> newList) {
mOldList = oldList; mOldList = oldList;
mNewList = newList; mNewList = newList;
} }

View File

@@ -0,0 +1,30 @@
package com.android.settings.search;
import com.android.settings.utils.AsyncLoader;
import android.content.Context;
import java.util.List;
/**
* Loads a sorted list of Search results for a given query.
*/
public class SearchResultLoader extends AsyncLoader<List<? extends SearchResult>> {
private final String mQuery;
public SearchResultLoader(Context context, String query) {
super(context);
mQuery = query;
}
@Override
public List<? extends SearchResult> loadInBackground() {
SearchResultAggregator aggregator = SearchResultAggregator.getInstance();
return aggregator.fetchResults(getContext(), mQuery);
}
@Override
protected void onDiscardResult(List<? extends SearchResult> result) {
}
}

View File

@@ -18,87 +18,25 @@
package com.android.settings.search; package com.android.settings.search;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.IntDef;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.support.v7.util.DiffUtil; import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.search.ranking.SearchResultsRankerCallback;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> {
implements SearchResultsRankerCallback {
private static final String TAG = "SearchResultsAdapter";
@VisibleForTesting
static final String DB_RESULTS_LOADER_KEY = DatabaseResultLoader.class.getName();
@VisibleForTesting
static final String APP_RESULTS_LOADER_KEY = InstalledAppResultLoader.class.getName();
@VisibleForTesting
static final String ACCESSIBILITY_LOADER_KEY = AccessibilityServiceResultLoader.class.getName();
@VisibleForTesting
static final String INPUT_DEVICE_LOADER_KEY = InputDeviceResultLoader.class.getName();
@VisibleForTesting
static final int MSG_RANKING_TIMED_OUT = 1;
private final SearchFragment mFragment; private final SearchFragment mFragment;
private final Context mContext;
private final List<SearchResult> mSearchResults; private final List<SearchResult> mSearchResults;
private final List<SearchResult> mStaticallyRankedSearchResults;
private Map<String, Set<? extends SearchResult>> mResultsMap;
private final SearchFeatureProvider mSearchFeatureProvider;
private List<Pair<String, Float>> mSearchRankingScores;
private Handler mHandler;
private boolean mSearchResultsLoaded;
private boolean mSearchResultsUpdated;
@IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT}) public SearchResultsAdapter(SearchFragment fragment) {
@Retention(RetentionPolicy.SOURCE)
private @interface AsyncRankingState {}
@VisibleForTesting
static final int DISABLED = 0;
@VisibleForTesting
static final int PENDING_RESULTS = 1;
@VisibleForTesting
static final int SUCCEEDED = 2;
@VisibleForTesting
static final int FAILED = 3;
@VisibleForTesting
static final int TIMED_OUT = 4;
private @AsyncRankingState int mAsyncRankingState;
public SearchResultsAdapter(SearchFragment fragment,
SearchFeatureProvider searchFeatureProvider) {
mFragment = fragment; mFragment = fragment;
mContext = fragment.getContext().getApplicationContext();
mSearchResults = new ArrayList<>(); mSearchResults = new ArrayList<>();
mResultsMap = new ArrayMap<>();
mSearchRankingScores = new ArrayList<>();
mStaticallyRankedSearchResults = new ArrayList<>();
mSearchFeatureProvider = searchFeatureProvider;
setHasStableIds(true); setHasStableIds(true);
} }
@@ -149,298 +87,30 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
return mSearchResults.size(); return mSearchResults.size();
} }
@MainThread
@Override
public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores) {
// Received the scores, stop the timeout timer.
getHandler().removeMessages(MSG_RANKING_TIMED_OUT);
if (mAsyncRankingState == PENDING_RESULTS) {
mAsyncRankingState = SUCCEEDED;
mSearchRankingScores.clear();
mSearchRankingScores.addAll(searchRankingScores);
if (canUpdateSearchResults()) {
updateSearchResults();
}
} else {
Log.w(TAG, "Ranking scores became available in invalid state: " + mAsyncRankingState);
}
}
@MainThread
@Override
public void onRankingFailed() {
if (mAsyncRankingState == PENDING_RESULTS) {
mAsyncRankingState = FAILED;
if (canUpdateSearchResults()) {
updateSearchResults();
}
} else {
Log.w(TAG, "Ranking scores failed in invalid states: " + mAsyncRankingState);
}
}
/**
* Store the results from each of the loaders to be merged when all loaders are finished.
*
* @param results the results from the loader.
* @param loaderClassName class name of the loader.
*/
@MainThread
public void addSearchResults(Set<? extends SearchResult> results, String loaderClassName) {
if (results == null) {
return;
}
mResultsMap.put(loaderClassName, results);
}
/** /**
* Displays recent searched queries. * Displays recent searched queries.
*
* @return The number of saved queries to display
*/ */
public int displaySavedQuery(List<? extends SearchResult> data) { public void displaySavedQuery(List<? extends SearchResult> data) {
clearResults(); clearResults();
mSearchResults.addAll(data); mSearchResults.addAll(data);
notifyDataSetChanged(); notifyDataSetChanged();
return mSearchResults.size();
}
/**
* Notifies the adapter that all the unsorted results are loaded and now the ladapter can
* proceed with ranking the results.
*/
@MainThread
public void notifyResultsLoaded() {
mSearchResultsLoaded = true;
// static ranking is skipped only if asyc ranking is already succeeded.
if (mAsyncRankingState != SUCCEEDED) {
doStaticRanking();
}
if (canUpdateSearchResults()) {
updateSearchResults();
}
} }
public void clearResults() { public void clearResults() {
mSearchResults.clear(); mSearchResults.clear();
mStaticallyRankedSearchResults.clear();
mResultsMap.clear();
notifyDataSetChanged(); notifyDataSetChanged();
} }
@VisibleForTesting
public List<SearchResult> getSearchResults() { public List<SearchResult> getSearchResults() {
return mSearchResults; return mSearchResults;
} }
@MainThread public void postSearchResults(List<? extends SearchResult> newSearchResults) {
public void initializeSearch(String query) {
clearResults();
mSearchResultsLoaded = false;
mSearchResultsUpdated = false;
if (mSearchFeatureProvider.isSmartSearchRankingEnabled(mContext)) {
mAsyncRankingState = PENDING_RESULTS;
mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
final Handler handler = getHandler();
final long timeoutMs = mSearchFeatureProvider.smartSearchRankingTimeoutMs(mContext);
handler.sendMessageDelayed(
handler.obtainMessage(MSG_RANKING_TIMED_OUT), timeoutMs);
mSearchFeatureProvider.querySearchResults(mContext, query, this);
} else {
mAsyncRankingState = DISABLED;
}
}
@AsyncRankingState int getAsyncRankingState() {
return mAsyncRankingState;
}
/**
* Merge the results from each of the loaders into one list for the adapter.
* Prioritizes results from the local database over installed apps.
*/
private void doStaticRanking() {
List<? extends SearchResult> databaseResults =
getSortedLoadedResults(DB_RESULTS_LOADER_KEY);
List<? extends SearchResult> installedAppResults =
getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
List<? extends SearchResult> accessibilityResults =
getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY);
List<? extends SearchResult> inputDeviceResults =
getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY);
int dbSize = databaseResults.size();
int appSize = installedAppResults.size();
int a11ySize = accessibilityResults.size();
int inputDeviceSize = inputDeviceResults.size();
int dbIndex = 0;
int appIndex = 0;
int a11yIndex = 0;
int inputDeviceIndex = 0;
int rank = SearchResult.TOP_RANK;
// TODO: We need a helper method to do k-way merge.
mStaticallyRankedSearchResults.clear();
while (rank <= SearchResult.BOTTOM_RANK) {
while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) {
mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++));
}
while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++));
}
while ((a11yIndex < a11ySize) && (accessibilityResults.get(a11yIndex).rank == rank)) {
mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++));
}
while (inputDeviceIndex < inputDeviceSize
&& inputDeviceResults.get(inputDeviceIndex).rank == rank) {
mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++));
}
rank++;
}
while (dbIndex < dbSize) {
mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++));
}
while (appIndex < appSize) {
mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++));
}
while(a11yIndex < a11ySize) {
mStaticallyRankedSearchResults.add(accessibilityResults.get(a11yIndex++));
}
while (inputDeviceIndex < inputDeviceSize) {
mStaticallyRankedSearchResults.add(inputDeviceResults.get(inputDeviceIndex++));
}
}
private void updateSearchResults() {
switch (mAsyncRankingState) {
case PENDING_RESULTS:
break;
case DISABLED:
case FAILED:
case TIMED_OUT:
// When DISABLED or FAILED or TIMED_OUT, we use static ranking results.
postSearchResults(mStaticallyRankedSearchResults, false);
break;
case SUCCEEDED:
postSearchResults(doAsyncRanking(), true);
break;
}
}
private boolean canUpdateSearchResults() {
// Results are not updated yet and db results are loaded and we are not waiting on async
// ranking scores.
return !mSearchResultsUpdated
&& mSearchResultsLoaded
&& mAsyncRankingState != PENDING_RESULTS;
}
@VisibleForTesting
List<SearchResult> doAsyncRanking() {
Set<? extends SearchResult> databaseResults =
getUnsortedLoadedResults(DB_RESULTS_LOADER_KEY);
List<? extends SearchResult> installedAppResults =
getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
List<? extends SearchResult> accessibilityResults =
getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY);
List<? extends SearchResult> inputDeviceResults =
getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY);
int dbSize = databaseResults.size();
int appSize = installedAppResults.size();
int a11ySize = accessibilityResults.size();
int inputDeviceSize = inputDeviceResults.size();
final List<SearchResult> asyncRankingResults = new ArrayList<>(
dbSize + appSize + a11ySize + inputDeviceSize);
TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
new Comparator<SearchResult>() {
@Override
public int compare(SearchResult o1, SearchResult o2) {
float score1 = getRankingScoreByStableId(o1.stableId);
float score2 = getRankingScoreByStableId(o2.stableId);
if (score1 > score2) {
return -1;
} else if (score1 == score2) {
return 0;
} else {
return 1;
}
}
});
dbResultsSortedByScores.addAll(databaseResults);
asyncRankingResults.addAll(dbResultsSortedByScores);
// Other results are not ranked by async ranking and appended at the end of the list.
asyncRankingResults.addAll(installedAppResults);
asyncRankingResults.addAll(accessibilityResults);
asyncRankingResults.addAll(inputDeviceResults);
return asyncRankingResults;
}
@VisibleForTesting
Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) {
return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<>();
}
@VisibleForTesting
List<? extends SearchResult> getSortedLoadedResults(String loaderKey) {
List<? extends SearchResult> sortedLoadedResults =
new ArrayList<>(getUnsortedLoadedResults(loaderKey));
Collections.sort(sortedLoadedResults);
return sortedLoadedResults;
}
/**
* Looks up ranking score for stableId
* @param stableId String of stableId
* @return the ranking score corresponding to the given stableId. If there is no score
* available for this stableId, -Float.MAX_VALUE is returned.
*/
@VisibleForTesting
Float getRankingScoreByStableId(int stableId) {
for (Pair<String, Float> rankingScore : mSearchRankingScores) {
if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) {
return rankingScore.second;
}
}
// If stableId not found in the list, we assign the minimum score so it will appear at
// the end of the list.
Log.w(TAG, "stableId " + stableId + " was not in the ranking scores.");
return -Float.MAX_VALUE;
}
@VisibleForTesting
Handler getHandler() {
if (mHandler == null) {
mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_RANKING_TIMED_OUT) {
mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
if (mAsyncRankingState == PENDING_RESULTS) {
mAsyncRankingState = TIMED_OUT;
if (canUpdateSearchResults()) {
updateSearchResults();
}
} else {
Log.w(TAG, "Ranking scores timed out in invalid state: " +
mAsyncRankingState);
}
}
}
};
}
return mHandler;
}
@VisibleForTesting
public void postSearchResults(List<SearchResult> newSearchResults, boolean detectMoves) {
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(
new SearchResultDiffCallback(mSearchResults, newSearchResults), detectMoves); new SearchResultDiffCallback(mSearchResults, newSearchResults));
mSearchResults.clear(); mSearchResults.clear();
mSearchResults.addAll(newSearchResults); mSearchResults.addAll(newSearchResults);
diffResult.dispatchUpdatesTo(this); diffResult.dispatchUpdatesTo(this);
mFragment.onSearchResultsDisplayed(mSearchResults.size()); mFragment.onSearchResultsDisplayed(mSearchResults.size());
mSearchResultsUpdated = true;
} }
} }

View File

@@ -17,6 +17,7 @@
package com.android.settings.search; package com.android.settings.search;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -46,7 +47,7 @@ import java.util.List;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class AccessibilityServiceResultLoaderTest { public class AccessibilityServiceResultFutureTaskTest {
private static final String QUERY = "test_query"; private static final String QUERY = "test_query";
@@ -59,7 +60,7 @@ public class AccessibilityServiceResultLoaderTest {
@Mock @Mock
private SiteMapManager mSiteMapManager; private SiteMapManager mSiteMapManager;
private AccessibilityServiceResultLoader mLoader; private AccessibilityServiceResultLoader.AccessibilityServiceResultCallable mCallable;
@Before @Before
public void setUp() { public void setUp() {
@@ -68,19 +69,20 @@ public class AccessibilityServiceResultLoaderTest {
.thenReturn(mAccessibilityManager); .thenReturn(mAccessibilityManager);
when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mContext.getPackageManager()).thenReturn(mPackageManager);
mLoader = new AccessibilityServiceResultLoader(mContext, QUERY, mSiteMapManager); mCallable = new AccessibilityServiceResultLoader.AccessibilityServiceResultCallable(
mContext, QUERY, mSiteMapManager);
} }
@Test @Test
public void query_noService_shouldNotReturnAnything() { public void query_noService_shouldNotReturnAnything() throws Exception {
assertThat(mLoader.loadInBackground()).isEmpty(); assertThat(mCallable.call()).isEmpty();
} }
@Test @Test
public void query_hasServiceMatchingTitle_shouldReturnResult() { public void query_hasServiceMatchingTitle_shouldReturnResult() throws Exception {
addFakeAccessibilityService(); addFakeAccessibilityService();
List<? extends SearchResult> results = new ArrayList<>(mLoader.loadInBackground()); List<? extends SearchResult> results = mCallable.call();
assertThat(results).hasSize(1); assertThat(results).hasSize(1);
SearchResult result = results.get(0); SearchResult result = results.get(0);
@@ -88,13 +90,14 @@ public class AccessibilityServiceResultLoaderTest {
} }
@Test @Test
public void query_serviceDoesNotMatchTitle_shouldReturnResult() { public void query_serviceDoesNotMatchTitle_shouldReturnResult() throws Exception {
addFakeAccessibilityService(); addFakeAccessibilityService();
mLoader = new AccessibilityServiceResultLoader(mContext, mCallable = new AccessibilityServiceResultLoader.AccessibilityServiceResultCallable(
mContext,
QUERY + "no_match", mSiteMapManager); QUERY + "no_match", mSiteMapManager);
assertThat(mLoader.loadInBackground()).isEmpty(); assertThat(mCallable.call()).isEmpty();
} }
private void addFakeAccessibilityService() { private void addFakeAccessibilityService() {

View File

@@ -17,9 +17,11 @@
package com.android.settings.search; package com.android.settings.search;
import static android.content.Context.INPUT_METHOD_SERVICE; import static android.content.Context.INPUT_METHOD_SERVICE;
import static com.android.settings.search.InputDeviceResultLoader.PHYSICAL_KEYBOARD_FRAGMENT; import static com.android.settings.search.InputDeviceResultLoader.PHYSICAL_KEYBOARD_FRAGMENT;
import static com.android.settings.search.InputDeviceResultLoader.VIRTUAL_KEYBOARD_FRAGMENT; import static com.android.settings.search.InputDeviceResultLoader.VIRTUAL_KEYBOARD_FRAGMENT;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.verifyZeroInteractions;
@@ -58,7 +60,7 @@ import java.util.List;
shadows = { shadows = {
ShadowInputDevice.class ShadowInputDevice.class
}) })
public class InputDeviceResultLoaderTest { public class InputDeviceResultFutureTaskTest {
private static final String QUERY = "test_query"; private static final String QUERY = "test_query";
private static final List<String> PHYSICAL_KEYBOARD_BREADCRUMB; private static final List<String> PHYSICAL_KEYBOARD_BREADCRUMB;
@@ -84,7 +86,7 @@ public class InputDeviceResultLoaderTest {
@Mock @Mock
private PackageManager mPackageManager; private PackageManager mPackageManager;
private InputDeviceResultLoader mLoader; private InputDeviceResultLoader.InputDeviceResultCallable mCallable;
@Before @Before
public void setUp() { public void setUp() {
@@ -99,7 +101,8 @@ public class InputDeviceResultLoaderTest {
when(mContext.getString(anyInt())) when(mContext.getString(anyInt()))
.thenAnswer(invocation -> RuntimeEnvironment.application.getString( .thenAnswer(invocation -> RuntimeEnvironment.application.getString(
(Integer) invocation.getArguments()[0])); (Integer) invocation.getArguments()[0]));
mLoader = new InputDeviceResultLoader(mContext, QUERY, mSiteMapManager); mCallable = new InputDeviceResultLoader.InputDeviceResultCallable(mContext, QUERY,
mSiteMapManager);
} }
@After @After
@@ -108,18 +111,19 @@ public class InputDeviceResultLoaderTest {
} }
@Test @Test
public void query_noKeyboard_shouldNotReturnAnything() { public void query_noKeyboard_shouldNotReturnAnything() throws Exception {
assertThat(mLoader.loadInBackground()).isEmpty();
assertThat(mCallable.call()).isEmpty();
} }
@Test @Test
public void query_hasPhysicalKeyboard_match() { public void query_hasPhysicalKeyboard_match() throws Exception {
addPhysicalKeyboard(QUERY); addPhysicalKeyboard(QUERY);
when(mSiteMapManager.buildBreadCrumb(mContext, PHYSICAL_KEYBOARD_FRAGMENT, when(mSiteMapManager.buildBreadCrumb(mContext, PHYSICAL_KEYBOARD_FRAGMENT,
RuntimeEnvironment.application.getString(R.string.physical_keyboard_title))) RuntimeEnvironment.application.getString(R.string.physical_keyboard_title)))
.thenReturn(PHYSICAL_KEYBOARD_BREADCRUMB); .thenReturn(PHYSICAL_KEYBOARD_BREADCRUMB);
final List<SearchResult> results = new ArrayList<>(mLoader.loadInBackground()); final List<? extends SearchResult> results = mCallable.call();
assertThat(results).hasSize(1); assertThat(results).hasSize(1);
assertThat(results.get(0).title).isEqualTo(QUERY); assertThat(results.get(0).title).isEqualTo(QUERY);
@@ -128,13 +132,13 @@ public class InputDeviceResultLoaderTest {
} }
@Test @Test
public void query_hasVirtualKeyboard_match() { public void query_hasVirtualKeyboard_match() throws Exception {
addVirtualKeyboard(QUERY); addVirtualKeyboard(QUERY);
when(mSiteMapManager.buildBreadCrumb(mContext, VIRTUAL_KEYBOARD_FRAGMENT, when(mSiteMapManager.buildBreadCrumb(mContext, VIRTUAL_KEYBOARD_FRAGMENT,
RuntimeEnvironment.application.getString(R.string.add_virtual_keyboard))) RuntimeEnvironment.application.getString(R.string.add_virtual_keyboard)))
.thenReturn(VIRTUAL_KEYBOARD_BREADCRUMB); .thenReturn(VIRTUAL_KEYBOARD_BREADCRUMB);
final List<SearchResult> results = new ArrayList<>(mLoader.loadInBackground()); final List<? extends SearchResult> results = mCallable.call();
assertThat(results).hasSize(1); assertThat(results).hasSize(1);
assertThat(results.get(0).title).isEqualTo(QUERY); assertThat(results.get(0).title).isEqualTo(QUERY);
assertThat(results.get(0).breadcrumbs) assertThat(results.get(0).breadcrumbs)
@@ -142,11 +146,11 @@ public class InputDeviceResultLoaderTest {
} }
@Test @Test
public void query_hasPhysicalVirtualKeyboard_doNotMatch() { public void query_hasPhysicalVirtualKeyboard_doNotMatch() throws Exception {
addPhysicalKeyboard("abc"); addPhysicalKeyboard("abc");
addVirtualKeyboard("def"); addVirtualKeyboard("def");
assertThat(mLoader.loadInBackground()).isEmpty(); assertThat(mCallable.call()).isEmpty();
verifyZeroInteractions(mSiteMapManager); verifyZeroInteractions(mSiteMapManager);
} }
@@ -170,4 +174,4 @@ public class InputDeviceResultLoaderTest {
when(mImm.getInputMethodList()).thenReturn(imis); when(mImm.getInputMethodList()).thenReturn(imis);
} }
} }

View File

@@ -19,7 +19,9 @@ package com.android.settings.search;
import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyList;
@@ -53,8 +55,6 @@ import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import java.util.ArrayList; import java.util.ArrayList;
@@ -77,7 +77,7 @@ public class InstalledAppResultLoaderTest {
@Mock @Mock
private SiteMapManager mSiteMapManager; private SiteMapManager mSiteMapManager;
private InstalledAppResultLoader mLoader; private InstalledAppResultLoader.InstalledAppResultCallable mCallable;
@Before @Before
public void setUp() { public void setUp() {
@@ -109,49 +109,50 @@ public class InstalledAppResultLoaderTest {
} }
@Test @Test
public void query_noMatchingQuery_shouldReturnEmptyResult() { public void query_noMatchingQuery_shouldReturnEmptyResult() throws Exception {
final String query = "abc"; final String query = "abc";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground()).isEmpty(); assertThat(mCallable.call()).isEmpty();
} }
@Test @Test
public void query_matchingQuery_shouldReturnNonSystemApps() { public void query_matchingQuery_shouldReturnNonSystemApps() throws Exception {
final String query = "app"; final String query = "app";
mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = spy(new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager)); mSiteMapManager));
when(mLoader.getContext()).thenReturn(mContext);
when(mSiteMapManager.buildBreadCrumb(eq(mContext), anyString(), anyString())) when(mSiteMapManager.buildBreadCrumb(eq(mContext), anyString(), anyString()))
.thenReturn(Arrays.asList(new String[]{"123"})); .thenReturn(Arrays.asList(new String[]{"123"}));
assertThat(mLoader.loadInBackground().size()).isEqualTo(3); assertThat(mCallable.call()).hasSize(3);
verify(mSiteMapManager) verify(mSiteMapManager)
.buildBreadCrumb(eq(mContext), anyString(), anyString()); .buildBreadCrumb(eq(mContext), anyString(), anyString());
} }
@Test @Test
public void query_matchingQuery_shouldReturnSystemAppUpdates() { public void query_matchingQuery_shouldReturnSystemAppUpdates() throws Exception {
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_UPDATED_SYSTEM_APP, ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_UPDATED_SYSTEM_APP,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
final String query = "app"; final String query = "app";
mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = spy(new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager)); mSiteMapManager));
when(mLoader.getContext()).thenReturn(mContext);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
verify(mSiteMapManager) verify(mSiteMapManager)
.buildBreadCrumb(eq(mContext), anyString(), anyString()); .buildBreadCrumb(eq(mContext), anyString(), anyString());
} }
@Test @Test
public void query_matchingQuery_shouldReturnSystemAppIfLaunchable() { public void query_matchingQuery_shouldReturnSystemAppIfLaunchable() throws Exception {
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM, ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM,
@@ -164,14 +165,15 @@ public class InstalledAppResultLoaderTest {
final String query = "app"; final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_matchingQuery_shouldReturnSystemAppIfHomeApp() { public void query_matchingQuery_shouldReturnSystemAppIfHomeApp() throws Exception {
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM, ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM,
@@ -180,28 +182,26 @@ public class InstalledAppResultLoaderTest {
any(Intent.class), anyInt(), anyInt())) any(Intent.class), anyInt(), anyInt()))
.thenReturn(null); .thenReturn(null);
when(mPackageManagerWrapper.getHomeActivities(anyList())).thenAnswer(new Answer<Object>() { when(mPackageManagerWrapper.getHomeActivities(anyList())).thenAnswer(invocation -> {
@Override final List<ResolveInfo> list = (List<ResolveInfo>) invocation.getArguments()[0];
public Object answer(InvocationOnMock invocation) throws Throwable { final ResolveInfo info = new ResolveInfo();
final List<ResolveInfo> list = (List<ResolveInfo>) invocation.getArguments()[0]; info.activityInfo = new ActivityInfo();
final ResolveInfo info = new ResolveInfo(); info.activityInfo.packageName = "app1";
info.activityInfo = new ActivityInfo(); list.add(info);
info.activityInfo.packageName = "app1"; return null;
list.add(info);
return null;
}
}); });
final String query = "app"; final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_matchingQuery_shouldNotReturnSystemAppIfNotLaunchable() { public void query_matchingQuery_shouldNotReturnSystemAppIfNotLaunchable() throws Exception {
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM, ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM,
@@ -212,21 +212,23 @@ public class InstalledAppResultLoaderTest {
final String query = "app"; final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground()).isEmpty(); assertThat(mCallable.call()).isEmpty();
verify(mSiteMapManager, never()) verify(mSiteMapManager, never())
.buildBreadCrumb(eq(mContext), anyString(), anyString()); .buildBreadCrumb(eq(mContext), anyString(), anyString());
} }
@Test @Test
public void query_matchingQuery_multipleResults() { public void query_matchingQuery_multipleResults() throws Exception {
final String query = "app"; final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
final Set<? extends SearchResult> results = mLoader.loadInBackground(); final List<? extends SearchResult> results = mCallable.call();
Set<CharSequence> expectedTitles = new HashSet<>(Arrays.asList("app4", "app", "appBuffer")); Set<CharSequence> expectedTitles = new HashSet<>(Arrays.asList("app4", "app", "appBuffer"));
Set<CharSequence> actualTitles = new HashSet<>(); Set<CharSequence> actualTitles = new HashSet<>();
@@ -237,161 +239,172 @@ public class InstalledAppResultLoaderTest {
} }
@Test @Test
public void query_normalWord_MatchPrefix() { public void query_normalWord_MatchPrefix() throws Exception {
final String query = "ba"; final String query = "ba";
final String packageName = "Bananas"; final String packageName = "Bananas";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_CapitalCase_DoestMatchSecondWord() { public void query_CapitalCase_DoestMatchSecondWord() throws Exception {
final String query = "Apples"; final String query = "Apples";
final String packageName = "BananasApples"; final String packageName = "BananasApples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(0); assertThat(mCallable.call()).isEmpty();
} }
@Test @Test
public void query_TwoWords_MatchesFirstWord() { public void query_TwoWords_MatchesFirstWord() throws Exception {
final String query = "Banana"; final String query = "Banana";
final String packageName = "Bananas Apples"; final String packageName = "Bananas Apples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_TwoWords_MatchesSecondWord() { public void query_TwoWords_MatchesSecondWord() throws Exception {
final String query = "Apple"; final String query = "Apple";
final String packageName = "Bananas Apples"; final String packageName = "Bananas Apples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_ThreeWords_MatchesThirdWord() { public void query_ThreeWords_MatchesThirdWord() throws Exception {
final String query = "Pear"; final String query = "Pear";
final String packageName = "Bananas Apples Pears"; final String packageName = "Bananas Apples Pears";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_DoubleSpacedWords_MatchesSecondWord() { public void query_DoubleSpacedWords_MatchesSecondWord() throws Exception {
final String query = "Apple"; final String query = "Apple";
final String packageName = "Bananas Apples"; final String packageName = "Bananas Apples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_SpecialChar_MatchesSecondWord() { public void query_SpecialChar_MatchesSecondWord() throws Exception {
final String query = "Apple"; final String query = "Apple";
final String packageName = "Bananas & Apples"; final String packageName = "Bananas & Apples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_TabSeparated_MatchesSecondWord() { public void query_TabSeparated_MatchesSecondWord() throws Exception {
final String query = "Apple"; final String query = "Apple";
final String packageName = "Bananas\tApples"; final String packageName = "Bananas\tApples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_LeadingNumber_MatchesWord() { public void query_LeadingNumber_MatchesWord() throws Exception {
final String query = "4"; final String query = "4";
final String packageName = "4Bananas"; final String packageName = "4Bananas";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1); assertThat(mCallable.call()).hasSize(1);
} }
@Test @Test
public void query_FirstWordPrefixOfQuery_NoMatch() { public void query_FirstWordPrefixOfQuery_NoMatch() throws Exception {
final String query = "Bananass"; final String query = "Bananass";
final String packageName = "Bananas Apples"; final String packageName = "Bananas Apples";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(0); assertThat(mCallable.call()).isEmpty();
} }
@Test @Test
public void query_QueryLongerThanAppName_NoMatch() { public void query_QueryLongerThanAppName_NoMatch() throws Exception {
final String query = "BananasApples"; final String query = "BananasApples";
final String packageName = "Bananas"; final String packageName = "Bananas";
when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
.thenReturn(Arrays.asList( .thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(0); assertThat(mCallable.call()).isEmpty();
} }
@Test @Test
public void query_appExistsInBothProfiles() { public void query_appExistsInBothProfiles() throws Exception {
final String query = "carrot"; final String query = "carrot";
final String packageName = "carrot"; final String packageName = "carrot";
final int user1 = 0; final int user1 = 0;
@@ -414,10 +427,11 @@ public class InstalledAppResultLoaderTest {
packageName, 0 /* flags */, packageName, 0 /* flags */,
0 /* targetSdkVersion */))); 0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext,
mPackageManagerWrapper, query,
mSiteMapManager); mSiteMapManager);
Set<AppSearchResult> searchResults = (Set<AppSearchResult>) mLoader.loadInBackground(); List<AppSearchResult> searchResults = (List<AppSearchResult>) mCallable.call();
assertThat(searchResults).hasSize(2); assertThat(searchResults).hasSize(2);
Set<Integer> uidResults = searchResults.stream().map(result -> result.info.uid).collect( Set<Integer> uidResults = searchResults.stream().map(result -> result.info.uid).collect(

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.search;
import android.content.Context;
import java.util.HashSet;
import java.util.Set;
public class MockAccessibilityLoader extends AccessibilityServiceResultLoader {
public MockAccessibilityLoader(Context context) {
super(context, "test_query", null);
}
@Override
public Set<? extends SearchResult> loadInBackground() {
return new HashSet<>();
}
@Override
protected void onDiscardResult(Set<? extends SearchResult> result) {
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.settings.search;
import android.content.Context;
import com.android.settings.search.InstalledAppResultLoader;
import com.android.settings.search.SearchResult;
import java.util.HashSet;
import java.util.Set;
/**
* Mock loader to subvert the requirements of returning data while also driving the Loader
* lifecycle.
*/
class MockAppLoader extends InstalledAppResultLoader {
public MockAppLoader(Context context) {
super(context, null, "", null);
}
@Override
public Set<? extends SearchResult> loadInBackground() {
return new HashSet<>();
}
@Override
protected void onDiscardResult(Set<? extends SearchResult> result) {
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.android.settings.search;
import android.content.Context;
import com.android.settings.search.DatabaseResultLoader;
import com.android.settings.search.SearchResult;
import java.util.HashSet;
import java.util.Set;
/**
* Mock loader to subvert the requirements of returning data while also driving the Loader
* lifecycle.
*/
class MockDBLoader extends DatabaseResultLoader {
public MockDBLoader(Context context) {
super(context, "test", null);
}
@Override
public Set<? extends SearchResult> loadInBackground() {
return new HashSet<>();
}
@Override
protected void onDiscardResult(Set<? extends SearchResult> result) {
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.search;
import android.content.Context;
import java.util.HashSet;
import java.util.Set;
public class MockInputDeviceResultLoader extends InputDeviceResultLoader {
public MockInputDeviceResultLoader(Context context) {
super(context, "test_query", null);
}
@Override
public Set<? extends SearchResult> loadInBackground() {
return new HashSet<>();
}
@Override
protected void onDiscardResult(Set<? extends SearchResult> result) {
}
}

View File

@@ -0,0 +1,29 @@
package com.android.settings.search;
import android.content.Context;
import com.android.settings.search.SearchResult;
import com.android.settings.search.SearchResultLoader;
import java.util.ArrayList;
import java.util.List;
/**
* Mock loader to subvert the requirements of returning data while also driving the Loader
* lifecycle.
*/
public class MockSearchResultLoader extends SearchResultLoader {
public MockSearchResultLoader(Context context) {
super(context, "test");
}
@Override
public List<? extends SearchResult> loadInBackground() {
return new ArrayList<>();
}
@Override
protected void onDiscardResult(List<? extends SearchResult> result) {
}
}

View File

@@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName; import android.content.ComponentName;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.SettingsRobolectricTestRunner;
@@ -33,6 +32,10 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SearchFeatureProviderImplTest { public class SearchFeatureProviderImplTest {
@@ -43,7 +46,7 @@ public class SearchFeatureProviderImplTest {
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mActivity = Robolectric.buildActivity(Activity.class).create().visible().get(); mActivity = Robolectric.buildActivity(Activity.class).create().visible().get();
mProvider = new SearchFeatureProviderImpl(); mProvider = spy(new SearchFeatureProviderImpl());
} }
@Test @Test
@@ -57,18 +60,19 @@ public class SearchFeatureProviderImplTest {
@Test @Test
public void getDatabaseSearchLoader_shouldCleanupQuery() { public void getDatabaseSearchLoader_shouldCleanupQuery() {
final String query = " space "; final String query = " space ";
final DatabaseResultLoader loader = mProvider.getDatabaseSearchLoader(mActivity, query);
assertThat(loader.mQueryText).isEqualTo(query.trim()); mProvider.getStaticSearchResultTask(mActivity, query);
verify(mProvider).cleanQuery(eq(query));
} }
@Test @Test
public void getInstalledAppSearchLoader_shouldCleanupQuery() { public void getInstalledAppSearchLoader_shouldCleanupQuery() {
final String query = " space "; final String query = " space ";
final InstalledAppResultLoader loader =
mProvider.getInstalledAppSearchLoader(mActivity, query);
assertThat(loader.mQuery).isEqualTo(query.trim()); mProvider.getInstalledAppSearchTask(mActivity, query);
verify(mProvider).cleanQuery(eq(query));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@@ -87,4 +91,12 @@ public class SearchFeatureProviderImplTest {
final ComponentName cn = new ComponentName(mActivity.getPackageName(), "class"); final ComponentName cn = new ComponentName(mActivity.getPackageName(), "class");
mProvider.verifyLaunchSearchResultPageCaller(mActivity, cn); mProvider.verifyLaunchSearchResultPageCaller(mActivity, cn);
} }
@Test
public void cleanQuery_trimsWhitespace() {
final String query = " space ";
final String cleanQuery = "space";
assertThat(mProvider.cleanQuery(query)).isEqualTo(cleanQuery);
}
} }

View File

@@ -18,6 +18,7 @@
package com.android.settings.search; package com.android.settings.search;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyInt;
@@ -54,9 +55,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Answers; import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher; import org.mockito.ArgumentMatcher;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
@@ -65,7 +64,7 @@ import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers; import org.robolectric.util.ReflectionHelpers;
import java.util.Set; import java.util.List;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, @Config(manifest = TestConfig.MANIFEST_PATH,
@@ -79,22 +78,13 @@ public class SearchFragmentTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext; private Context mContext;
@Mock @Mock
private DatabaseResultLoader mDatabaseResultLoader; private SearchResultLoader mSearchResultLoader;
@Mock
private InstalledAppResultLoader mInstalledAppResultLoader;
@Mock
private AccessibilityServiceResultLoader mAccessibilityServiceResultLoader;
@Mock
private InputDeviceResultLoader mInputDeviceResultLoader;
@Mock @Mock
private SavedQueryLoader mSavedQueryLoader; private SavedQueryLoader mSavedQueryLoader;
@Mock @Mock
private SavedQueryController mSavedQueryController; private SavedQueryController mSavedQueryController;
@Mock @Mock
private SearchResultsAdapter mSearchResultsAdapter; private SearchResultsAdapter mSearchResultsAdapter;
@Captor
private ArgumentCaptor<String> mQueryCaptor = ArgumentCaptor.forClass(String.class);
private FakeFeatureFactory mFeatureFactory; private FakeFeatureFactory mFeatureFactory;
@@ -113,17 +103,8 @@ public class SearchFragmentTest {
@Test @Test
public void screenRotate_shouldPersistQuery() { public void screenRotate_shouldPersistQuery() {
when(mFeatureFactory.searchFeatureProvider when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString())) .getSearchResultLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader); .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getAccessibilityServiceResultLoader(any(Context.class), anyString()))
.thenReturn(mAccessibilityServiceResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInputDeviceResultLoader(any(Context.class), anyString()))
.thenReturn(mInputDeviceResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
@@ -168,25 +149,16 @@ public class SearchFragmentTest {
activityController.setup(bundle); activityController.setup(bundle);
verify(mFeatureFactory.searchFeatureProvider, never()) verify(mFeatureFactory.searchFeatureProvider, never())
.getDatabaseSearchLoader(any(Context.class), anyString()); .getStaticSearchResultTask(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider, never()) verify(mFeatureFactory.searchFeatureProvider, never())
.getInstalledAppSearchLoader(any(Context.class), anyString()); .getInstalledAppSearchTask(any(Context.class), anyString());
} }
@Test @Test
public void queryTextChange_shouldTriggerLoaderAndInitializeSearch() { public void queryTextChange_shouldTriggerLoader() {
when(mFeatureFactory.searchFeatureProvider when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString())) .getSearchResultLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader); .thenReturn(mSearchResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getAccessibilityServiceResultLoader(any(Context.class), anyString()))
.thenReturn(mAccessibilityServiceResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInputDeviceResultLoader(any(Context.class), anyString()))
.thenReturn(mInputDeviceResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
@@ -199,7 +171,6 @@ public class SearchFragmentTest {
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true); .thenReturn(true);
ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter);
fragment.onQueryTextChange(testQuery); fragment.onQueryTextChange(testQuery);
activityController.get().onBackPressed(); activityController.get().onBackPressed();
@@ -209,11 +180,7 @@ public class SearchFragmentTest {
any(Context.class), any(Context.class),
eq(MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY)); eq(MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY));
verify(mFeatureFactory.searchFeatureProvider) verify(mFeatureFactory.searchFeatureProvider)
.getDatabaseSearchLoader(any(Context.class), anyString()); .getSearchResultLoader(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider)
.getInstalledAppSearchLoader(any(Context.class), anyString());
verify(mSearchResultsAdapter).initializeSearch(mQueryCaptor.capture());
assertThat(mQueryCaptor.getValue()).isEqualTo(testQuery);
} }
@Test @Test
@@ -238,18 +205,6 @@ public class SearchFragmentTest {
@Test @Test
public void queryTextChangeToEmpty_shouldLoadSavedQueryAndNotInitializeSearch() { public void queryTextChangeToEmpty_shouldLoadSavedQueryAndNotInitializeSearch() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getAccessibilityServiceResultLoader(any(Context.class), anyString()))
.thenReturn(mAccessibilityServiceResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInputDeviceResultLoader(any(Context.class), anyString()))
.thenReturn(mInputDeviceResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController = ActivityController<SearchActivity> activityController =
@@ -266,27 +221,14 @@ public class SearchFragmentTest {
fragment.onQueryTextChange(""); fragment.onQueryTextChange("");
verify(mFeatureFactory.searchFeatureProvider, never()) verify(mFeatureFactory.searchFeatureProvider, never())
.getDatabaseSearchLoader(any(Context.class), anyString()); .getStaticSearchResultTask(any(Context.class), anyString());
verify(mFeatureFactory.searchFeatureProvider, never()) verify(mFeatureFactory.searchFeatureProvider, never())
.getInstalledAppSearchLoader(any(Context.class), anyString()); .getInstalledAppSearchTask(any(Context.class), anyString());
verify(mSavedQueryController).loadSavedQueries(); verify(mSavedQueryController).loadSavedQueries();
verify(mSearchResultsAdapter, never()).initializeSearch(anyString());
} }
@Test @Test
public void updateIndex_TriggerOnCreate() { public void updateIndex_TriggerOnCreate() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getAccessibilityServiceResultLoader(any(Context.class), anyString()))
.thenReturn(mAccessibilityServiceResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInputDeviceResultLoader(any(Context.class), anyString()))
.thenReturn(mInputDeviceResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
@@ -303,41 +245,11 @@ public class SearchFragmentTest {
any(IndexingCallback.class)); any(IndexingCallback.class));
} }
@Test
public void syncLoaders_MergeWhenAllLoadersDone() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(new MockDBLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(new MockAppLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
.thenReturn(true);
fragment.onQueryTextChange("non-empty");
Robolectric.flushForegroundThreadScheduler();
verify(fragment, times(2)).onLoadFinished(any(Loader.class), any(Set.class));
}
@Test @Test
public void whenNoQuery_HideFeedbackIsCalled() { public void whenNoQuery_HideFeedbackIsCalled() {
when(mFeatureFactory.searchFeatureProvider when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString())) .getSearchResultLoader(any(Context.class), anyString()))
.thenReturn(new MockDBLoader(RuntimeEnvironment.application)); .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(new MockAppLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
@@ -359,17 +271,8 @@ public class SearchFragmentTest {
@Test @Test
public void onLoadFinished_ShowsFeedback() { public void onLoadFinished_ShowsFeedback() {
when(mFeatureFactory.searchFeatureProvider when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString())) .getSearchResultLoader(any(Context.class), anyString()))
.thenReturn(new MockDBLoader(RuntimeEnvironment.application)); .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(new MockAppLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getAccessibilityServiceResultLoader(any(Context.class), anyString()))
.thenReturn(new MockAccessibilityLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getInputDeviceResultLoader(any(Context.class), anyString()))
.thenReturn(new MockInputDeviceResultLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader); .thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController = ActivityController<SearchActivity> activityController =
@@ -413,9 +316,7 @@ public class SearchFragmentTest {
fragment.onIndexingFinished(); fragment.onIndexingFinished();
verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.DATABASE), verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.SEARCH_RESULT),
eq(null), any(LoaderManager.LoaderCallbacks.class));
verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.INSTALLED_APPS),
eq(null), any(LoaderManager.LoaderCallbacks.class)); eq(null), any(LoaderManager.LoaderCallbacks.class));
} }
@@ -480,16 +381,13 @@ public class SearchFragmentTest {
eq("test_setting"), eq("test_setting"),
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_COUNT)), argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_COUNT)),
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK)), argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK)),
argThat(pairMatches(MetricsProto.MetricsEvent
.FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE)),
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH))); argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH)));
verify(mFeatureFactory.searchFeatureProvider).searchResultClicked(nullable(Context.class), verify(mFeatureFactory.searchFeatureProvider).searchResultClicked(nullable(Context.class),
nullable(String.class), eq(searchResult)); nullable(String.class), eq(searchResult));
} }
@Test @Test
public void onResume_shouldCallSearchRankingWarmupIfSmartSearchRankingEnabled(){ public void onResume_shouldCallSearchRankingWarmupIfSmartSearchRankingEnabled() {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class)))
.thenReturn(true); .thenReturn(true);
@@ -504,7 +402,7 @@ public class SearchFragmentTest {
} }
@Test @Test
public void onResume_shouldNotCallSearchRankingWarmupIfSmartSearchRankingDisabled(){ public void onResume_shouldNotCallSearchRankingWarmupIfSmartSearchRankingDisabled() {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class))) when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class)))
.thenReturn(false); .thenReturn(false);

View File

@@ -0,0 +1,271 @@
package com.android.settings.search;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import com.android.settings.TestConfig;
import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class SearchResultAggregatorTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mContext;
private FakeFeatureFactory mFeatureFactory;
private SearchResultAggregator mAggregator;
@Mock
private DatabaseResultLoader mStaticTask;
@Mock
private InstalledAppResultLoader mAppTask;
@Mock
private InputDeviceResultLoader mInputTask;
@Mock
private AccessibilityServiceResultLoader mMAccessibilityTask;
@Mock
private ExecutorService mService;
private String[] DB_TITLES = {"static_one", "static_two"};
private String[] INPUT_TITLES = {"input_one", "input_two"};
private String[] ACCESS_TITLES = {"access_one", "access_two"};
private String[] APP_TITLES = {"app_one", "app_two"};
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mAggregator = spy(SearchResultAggregator.getInstance());
FakeFeatureFactory.setupForTest(mContext);
mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
// Return mock loaders from feature provider
when(mFeatureFactory.searchFeatureProvider.getStaticSearchResultTask(any(Context.class),
anyString())).thenReturn(mStaticTask);
when(mFeatureFactory.searchFeatureProvider.getInstalledAppSearchTask(any(Context.class),
anyString())).thenReturn(mAppTask);
when(mFeatureFactory.searchFeatureProvider.getInputDeviceResultTask(any(Context.class),
anyString())).thenReturn(mInputTask);
when(mFeatureFactory.searchFeatureProvider.getAccessibilityServiceResultTask(
any(Context.class),
anyString())).thenReturn(mMAccessibilityTask);
when(mFeatureFactory.searchFeatureProvider.getExecutorService()).thenReturn(mService);
// Return fake data from the loaders
List<? extends SearchResult> dbResults = getDummyDbResults();
doReturn(dbResults).when(mStaticTask).get(anyLong(), any(TimeUnit.class));
List<? extends SearchResult> appResults = getDummyAppResults();
doReturn(appResults).when(mAppTask).get(anyLong(), any(TimeUnit.class));
List<? extends SearchResult> inputResults = getDummyInputDeviceResults();
doReturn(inputResults).when(mInputTask).get(anyLong(), any(TimeUnit.class));
List<? extends SearchResult> accessResults = getDummyAccessibilityResults();
doReturn(accessResults).when(mMAccessibilityTask).get(anyLong(), any(TimeUnit.class));
}
@Test
public void testStaticResults_mergedProperly() {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
.thenReturn(false);
List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
assertThat(results).hasSize(8);
assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
assertThat(results.get(3).title).isEqualTo(ACCESS_TITLES[0]);
assertThat(results.get(4).title).isEqualTo(INPUT_TITLES[0]);
assertThat(results.get(5).title).isEqualTo(APP_TITLES[1]);
assertThat(results.get(6).title).isEqualTo(ACCESS_TITLES[1]);
assertThat(results.get(7).title).isEqualTo(INPUT_TITLES[1]);
}
@Test
public void testStaticRanking_staticThrowsException_dbResultsAreMissing() throws Exception {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
.thenReturn(false);
when(mStaticTask.get(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
assertThat(results).hasSize(6);
assertThat(results.get(0).title).isEqualTo(APP_TITLES[0]);
assertThat(results.get(1).title).isEqualTo(ACCESS_TITLES[0]);
assertThat(results.get(2).title).isEqualTo(INPUT_TITLES[0]);
assertThat(results.get(3).title).isEqualTo(APP_TITLES[1]);
assertThat(results.get(4).title).isEqualTo(ACCESS_TITLES[1]);
assertThat(results.get(5).title).isEqualTo(INPUT_TITLES[1]);
}
@Test
public void testStaticRanking_appsThrowException_appResultsAreMissing() throws Exception {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
.thenReturn(false);
when(mAppTask.get(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
assertThat(results).hasSize(6);
assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
assertThat(results.get(2).title).isEqualTo(ACCESS_TITLES[0]);
assertThat(results.get(3).title).isEqualTo(INPUT_TITLES[0]);
assertThat(results.get(4).title).isEqualTo(ACCESS_TITLES[1]);
assertThat(results.get(5).title).isEqualTo(INPUT_TITLES[1]);
}
@Test
public void testStaticRanking_inputThrowException_inputResultsAreMissing() throws Exception {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
.thenReturn(false);
when(mInputTask.get(anyLong(), any(TimeUnit.class))).thenThrow(new InterruptedException());
List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
assertThat(results).hasSize(6);
assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
assertThat(results.get(3).title).isEqualTo(ACCESS_TITLES[0]);
assertThat(results.get(4).title).isEqualTo(APP_TITLES[1]);
assertThat(results.get(5).title).isEqualTo(ACCESS_TITLES[1]);
}
@Test
public void testStaticRanking_accessThrowException_accessResultsAreMissing() throws Exception {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext))
.thenReturn(false);
when(mMAccessibilityTask.get(anyLong(), any(TimeUnit.class))).thenThrow(
new InterruptedException());
List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
assertThat(results).hasSize(6);
assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
assertThat(results.get(3).title).isEqualTo(INPUT_TITLES[0]);
assertThat(results.get(4).title).isEqualTo(APP_TITLES[1]);
assertThat(results.get(5).title).isEqualTo(INPUT_TITLES[1]);
}
@Test
public void testDynamicRanking_sortsWithDynamicRanking() {
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(
true);
List<? extends SearchResult> results = mAggregator.fetchResults(mContext, "test");
assertThat(results).hasSize(8);
assertThat(results.get(0).title).isEqualTo(DB_TITLES[0]);
assertThat(results.get(1).title).isEqualTo(DB_TITLES[1]);
assertThat(results.get(2).title).isEqualTo(APP_TITLES[0]);
assertThat(results.get(3).title).isEqualTo(ACCESS_TITLES[0]);
assertThat(results.get(4).title).isEqualTo(INPUT_TITLES[0]);
assertThat(results.get(5).title).isEqualTo(APP_TITLES[1]);
assertThat(results.get(6).title).isEqualTo(ACCESS_TITLES[1]);
assertThat(results.get(7).title).isEqualTo(INPUT_TITLES[1]);
}
private List<? extends SearchResult> getDummyDbResults() {
List<SearchResult> results = new ArrayList<>();
ResultPayload payload = new ResultPayload(new Intent());
SearchResult.Builder builder = new SearchResult.Builder();
builder.setPayload(payload)
.setTitle(DB_TITLES[0])
.setRank(1)
.setStableId(Objects.hash(DB_TITLES[0], "db"));
results.add(builder.build());
builder.setTitle(DB_TITLES[1])
.setRank(2)
.setStableId(Objects.hash(DB_TITLES[1], "db"));
results.add(builder.build());
return results;
}
private List<? extends SearchResult> getDummyAppResults() {
List<AppSearchResult> results = new ArrayList<>();
ResultPayload payload = new ResultPayload(new Intent());
AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setPayload(payload)
.setTitle(APP_TITLES[0])
.setRank(1)
.setStableId(Objects.hash(APP_TITLES[0], "app"));
results.add(builder.build());
builder.setTitle(APP_TITLES[1])
.setRank(2)
.setStableId(Objects.hash(APP_TITLES[1], "app"));
results.add(builder.build());
return results;
}
public List<? extends SearchResult> getDummyInputDeviceResults() {
List<SearchResult> results = new ArrayList<>();
ResultPayload payload = new ResultPayload(new Intent());
AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setPayload(payload)
.setTitle(INPUT_TITLES[0])
.setRank(1)
.setStableId(Objects.hash(INPUT_TITLES[0], "app"));
results.add(builder.build());
builder.setTitle(INPUT_TITLES[1])
.setRank(2)
.setStableId(Objects.hash(INPUT_TITLES[1], "app"));
results.add(builder.build());
return results;
}
public List<? extends SearchResult> getDummyAccessibilityResults() {
List<SearchResult> results = new ArrayList<>();
ResultPayload payload = new ResultPayload(new Intent());
AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setPayload(payload)
.setTitle(ACCESS_TITLES[0])
.setRank(1)
.setStableId(Objects.hash(ACCESS_TITLES[0], "app"));
results.add(builder.build());
builder.setTitle(ACCESS_TITLES[1])
.setRank(2)
.setStableId(Objects.hash(ACCESS_TITLES[1], "app"));
results.add(builder.build());
return results;
}
}

View File

@@ -18,44 +18,32 @@
package com.android.settings.search; package com.android.settings.search;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.util.Pair;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.android.settings.R;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.search.SearchResult.Builder;
import com.android.settings.search.ranking.SearchResultsRankerCallback;
import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.SettingsRobolectricTestRunner;
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;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
@RunWith(SettingsRobolectricTestRunner.class) @RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -67,25 +55,18 @@ public class SearchResultsAdapterTest {
private SearchFeatureProvider mSearchFeatureProvider; private SearchFeatureProvider mSearchFeatureProvider;
@Mock @Mock
private Context mMockContext; private Context mMockContext;
@Captor
private ArgumentCaptor<Integer> mSearchResultsCountCaptor =
ArgumentCaptor.forClass(Integer.class);
private SearchResultsAdapter mAdapter; private SearchResultsAdapter mAdapter;
private Context mContext; private Context mContext;
private String mLoaderClassName;
private String[] TITLES = {"alpha", "bravo", "charlie", "appAlpha", "appBravo", "appCharlie"};
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = Robolectric.buildActivity(Activity.class).get(); mContext = Robolectric.buildActivity(Activity.class).get();
mLoaderClassName = DatabaseResultLoader.class.getName();
when(mFragment.getContext()).thenReturn(mMockContext); when(mFragment.getContext()).thenReturn(mMockContext);
when(mMockContext.getApplicationContext()).thenReturn(mContext); when(mMockContext.getApplicationContext()).thenReturn(mContext);
when(mSearchFeatureProvider.smartSearchRankingTimeoutMs(any(Context.class))) when(mSearchFeatureProvider.smartSearchRankingTimeoutMs(any(Context.class)))
.thenReturn(300L); .thenReturn(300L);
mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider); mAdapter = new SearchResultsAdapter(mFragment);
} }
@Test @Test
@@ -94,17 +75,6 @@ public class SearchResultsAdapterTest {
assertThat(updatedResults).isEmpty(); assertThat(updatedResults).isEmpty();
} }
@Test
public void testSingleSourceMerge_exactCopyReturned() {
Set<SearchResult> intentResults = getIntentSampleResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(intentResults, mLoaderClassName);
mAdapter.notifyResultsLoaded();
List<SearchResult> updatedResults = mAdapter.getSearchResults();
assertThat(updatedResults).containsAllIn(intentResults);
}
@Test @Test
public void testCreateViewHolder_returnsIntentResult() { public void testCreateViewHolder_returnsIntentResult() {
ViewGroup group = new FrameLayout(mContext); ViewGroup group = new FrameLayout(mContext);
@@ -123,387 +93,13 @@ public class SearchResultsAdapterTest {
} }
@Test @Test
public void testEndToEndSearch_properResultsMerged_correctOrder() { public void testPostSearchResults_addsDataAndDisplays() {
mAdapter.initializeSearch(""); List<SearchResult> results = getDummyDbResults();
mAdapter.addSearchResults(new HashSet<>(getDummyAppResults()),
InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(new HashSet<>(getDummyDbResults()),
DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<SearchResult> results = mAdapter.getSearchResults(); mAdapter.postSearchResults(results);
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
}
@Test assertThat(mAdapter.getSearchResults()).containsExactlyElementsIn(results);
public void testEndToEndSearch_addResults_resultsAddedInOrder() { verify(mFragment).onSearchResultsDisplayed(anyInt());
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
// Add two individual items
mAdapter.addSearchResults(new HashSet<>(appResults.subList(0, 1)),
InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(new HashSet<>(dbResults.subList(0, 1)),
DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
// Add super-set of items
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
verify(mFragment, times(2)).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getAllValues().toArray())
.isEqualTo(new Integer[] {2, 6});
}
@Test
public void testEndToEndSearch_removeResults_resultsAdded() {
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
// Add list of items
mAdapter.initializeSearch("");
mAdapter.addSearchResults(new HashSet<>(appResults),
InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(new HashSet<>(dbResults),
DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
// Add subset of items
mAdapter.initializeSearch("");
mAdapter.addSearchResults(new HashSet<>(appResults.subList(0, 1)),
InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(new HashSet<>(dbResults.subList(0, 1)),
DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[0]);
assertThat(results.get(1).title).isEqualTo(TITLES[3]);
verify(mFragment, times(2)).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getAllValues().toArray())
.isEqualTo(new Integer[] {6, 2});
}
@Test
public void testEndToEndSearch_smartSearchRankingEnabledAndSucceededAfterResultsLoaded() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.SUCCEEDED);
}
@Test
public void testEndToEndSearch_smartSearchRankingEnabledAndSucceededBeforeResultsLoaded() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.SUCCEEDED);
}
@Test
public void testEndToEndSearch_smartSearchRankingEnabledAndFailedAfterResultsLoaded() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
mAdapter.onRankingFailed();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.FAILED);
}
@Test
public void testEndToEndSearch_smartSearchRankingEnabledAndFailedBeforeResultsLoaded() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.onRankingFailed();
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.FAILED);
}
@Test
public void testEndToEndSearch_smartSearchRankingEnabledAndTimedoutAfterResultsLoaded() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
waitUntilRankingTimesOut();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.TIMED_OUT);
}
@Test
public void testEndToEndSearch_smartSearchRankingEnabledAndTimedoutBeforeResultsLoaded() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
waitUntilRankingTimesOut();
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.TIMED_OUT);
}
@Test
public void testDoSmartRanking_shouldRankAppResultsAfterDbResults() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
List<SearchResult> results = mAdapter.doAsyncRanking();
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
}
@Test
public void testDoSmartRanking_shouldRankResultsWithMissingScoresAfterScoredResults() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
mAdapter.notifyResultsLoaded();
List<Pair<String, Float>> rankingScores = getDummyRankingScores();
rankingScores.remove(1); // no ranking score for alpha
mAdapter.onRankingScoresAvailable(rankingScores);
List<SearchResult> results = mAdapter.doAsyncRanking();
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
assertThat(results.get(1).title).isEqualTo(TITLES[1]); // bravo
assertThat(results.get(2).title).isEqualTo(TITLES[0]); // alpha
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
}
@Test
public void testGetUnsortedLoadedResults () {
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
Set<CharSequence> expectedDbTitles = new HashSet<>(
Arrays.asList("alpha", "bravo", "charlie"));
Set<CharSequence> expectedAppTitles = new HashSet<>(
Arrays.asList("appAlpha", "appBravo", "appCharlie"));
Set<CharSequence> actualDbTitles = new HashSet<>();
Set<CharSequence> actualAppTitles = new HashSet<>();
for (SearchResult result : mAdapter.getUnsortedLoadedResults(SearchResultsAdapter
.DB_RESULTS_LOADER_KEY)) {
actualDbTitles.add(result.title);
}
for (SearchResult result : mAdapter.getUnsortedLoadedResults(SearchResultsAdapter
.APP_RESULTS_LOADER_KEY)) {
actualAppTitles.add(result.title);
}
assertThat(actualDbTitles).isEqualTo(expectedDbTitles);
assertThat(actualAppTitles).isEqualTo(expectedAppTitles);
}
@Test
public void testGetSortedLoadedResults() {
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.addSearchResults(
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
mAdapter.addSearchResults(
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
List<? extends SearchResult> actualDbResults =
mAdapter.getSortedLoadedResults(SearchResultsAdapter.DB_RESULTS_LOADER_KEY);
List<? extends SearchResult> actualAppResults =
mAdapter.getSortedLoadedResults(SearchResultsAdapter.APP_RESULTS_LOADER_KEY);
assertThat(actualDbResults.get(0).title).isEqualTo(TITLES[0]); // charlie
assertThat(actualDbResults.get(1).title).isEqualTo(TITLES[1]); // bravo
assertThat(actualDbResults.get(2).title).isEqualTo(TITLES[2]); // alpha
assertThat(actualAppResults.get(0).title).isEqualTo(TITLES[3]); // appAlpha
assertThat(actualAppResults.get(1).title).isEqualTo(TITLES[4]); // appBravo
assertThat(actualAppResults.get(2).title).isEqualTo(TITLES[5]); // appCharlie
}
@Test
public void testInitializeSearch_shouldNotRunSmartRankingIfDisabled() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(false);
mAdapter.initializeSearch("");
mAdapter.notifyResultsLoaded();
verify(mSearchFeatureProvider, never()).querySearchResults(
any(Context.class), anyString(), any(SearchResultsRankerCallback.class));
assertThat(mAdapter.getAsyncRankingState()).isEqualTo(SearchResultsAdapter.DISABLED);
}
@Test
public void testInitialSearch_shouldRunSmartRankingIfEnabled() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
mAdapter.initializeSearch("");
mAdapter.notifyResultsLoaded();
verify(mSearchFeatureProvider, times(1)).querySearchResults(
any(Context.class), anyString(), any(SearchResultsRankerCallback.class));
assertThat(mAdapter.getAsyncRankingState())
.isEqualTo(SearchResultsAdapter.PENDING_RESULTS);
}
@Test
public void testGetRankingScoreByStableId() {
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
List<SearchResult> appResults = getDummyAppResults();
List<SearchResult> dbResults = getDummyDbResults();
mAdapter.initializeSearch("");
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(0).stableId))
.isWithin(1e-10f).of(0.8f);
assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(1).stableId))
.isWithin(1e-10f).of(0.2f);
assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(2).stableId))
.isWithin(1e-10f).of(0.9f);
assertThat(mAdapter.getRankingScoreByStableId(appResults.get(0).stableId))
.isEqualTo(-Float.MAX_VALUE);
assertThat(mAdapter.getRankingScoreByStableId(appResults.get(1).stableId))
.isEqualTo(-Float.MAX_VALUE);
assertThat(mAdapter.getRankingScoreByStableId(appResults.get(2).stableId))
.isEqualTo(-Float.MAX_VALUE);
}
private void waitUntilRankingTimesOut() {
while (mAdapter.getHandler().hasMessages(mAdapter.MSG_RANKING_TIMED_OUT)) {
try {
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
Thread.sleep(100);
} catch (InterruptedException e) {
// Do nothing
}
}
} }
private List<SearchResult> getDummyDbResults() { private List<SearchResult> getDummyDbResults() {
@@ -511,78 +107,21 @@ public class SearchResultsAdapterTest {
ResultPayload payload = new ResultPayload(new Intent()); ResultPayload payload = new ResultPayload(new Intent());
SearchResult.Builder builder = new SearchResult.Builder(); SearchResult.Builder builder = new SearchResult.Builder();
builder.setPayload(payload) builder.setPayload(payload)
.setTitle(TITLES[0]) .setTitle("one")
.setRank(1) .setRank(1)
.setStableId(Objects.hash(TITLES[0], "db")); .setStableId(Objects.hash("one", "db"));
results.add(builder.build()); results.add(builder.build());
builder.setTitle(TITLES[1]) builder.setTitle("two")
.setRank(3) .setRank(3)
.setStableId(Objects.hash(TITLES[1], "db")); .setStableId(Objects.hash("two", "db"));
results.add(builder.build()); results.add(builder.build());
builder.setTitle(TITLES[2]) builder.setTitle("three")
.setRank(6) .setRank(6)
.setStableId(Objects.hash(TITLES[2], "db")); .setStableId(Objects.hash("three", "db"));
results.add(builder.build()); results.add(builder.build());
return results; return results;
} }
private List<SearchResult> getDummyAppResults() {
List<SearchResult> results = new ArrayList<>();
ResultPayload payload = new ResultPayload(new Intent());
AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setPayload(payload)
.setTitle(TITLES[3])
.setRank(1)
.setStableId(Objects.hash(TITLES[3], "app"));
results.add(builder.build());
builder.setTitle(TITLES[4])
.setRank(2)
.setStableId(Objects.hash(TITLES[4], "app"));
results.add(builder.build());
builder.setTitle(TITLES[5])
.setRank(4)
.setStableId(Objects.hash(TITLES[5], "app"));
results.add(builder.build());
return results;
}
private Set<SearchResult> getIntentSampleResults() {
Set<SearchResult> sampleResults = new HashSet<>();
ArrayList<String> breadcrumbs = new ArrayList<>();
final Drawable icon = mContext.getDrawable(R.drawable.ic_search_24dp);
final ResultPayload payload = new ResultPayload(null);
final SearchResult.Builder builder = new Builder();
builder.setTitle("title")
.setSummary("summary")
.setRank(1)
.addBreadcrumbs(breadcrumbs)
.setIcon(icon)
.setPayload(payload)
.setStableId(Objects.hash("title", "summary", 1));
sampleResults.add(builder.build());
builder.setRank(2)
.setStableId(Objects.hash("title", "summary", 2));
sampleResults.add(builder.build());
builder.setRank(3)
.setStableId(Objects.hash("title", "summary", 3));
sampleResults.add(builder.build());
return sampleResults;
}
private List<Pair<String, Float>> getDummyRankingScores() {
List<SearchResult> results = getDummyDbResults();
List<Pair<String, Float>> scores = new ArrayList<>();
scores.add(new Pair<>(Long.toString(results.get(2).stableId), 0.9f)); // charlie
scores.add(new Pair<>(Long.toString(results.get(0).stableId), 0.8f)); // alpha
scores.add(new Pair<>(Long.toString(results.get(1).stableId), 0.2f)); // bravo
return scores;
}
} }

View File

@@ -21,9 +21,11 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.util.Pair;
import com.android.settings.TestConfig; import com.android.settings.TestConfig;
import com.android.settings.dashboard.SiteMapManager; import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.search.DatabaseResultLoader.StaticSearchResultCallable;
import com.android.settings.search.indexing.IndexData; import com.android.settings.search.indexing.IndexData;
import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.DatabaseTestUtils;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
@@ -39,37 +41,54 @@ import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment; import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import java.util.Arrays; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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)
public class DatabaseResultLoaderTest { public class StaticSearchResultFutureTaskTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS) @Mock(answer = Answers.RETURNS_DEEP_STUBS)
private Context mMockContext; private Context mMockContext;
@Mock @Mock
private SiteMapManager mSiteMapManager; private SiteMapManager mSiteMapManager;
@Mock
private ExecutorService mService;
private Context mContext; private Context mContext;
SQLiteDatabase mDb; SQLiteDatabase mDb;
FakeFeatureFactory mFeatureFactory;
private final String[] STATIC_TITLES = {"static one", "static two", "static three"};
private final int[] STABLE_IDS =
{"id_one".hashCode(), "id_two".hashCode(), "id_three".hashCode()};
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application; mContext = RuntimeEnvironment.application;
FakeFeatureFactory.setupForTest(mMockContext); mFeatureFactory = FakeFeatureFactory.setupForTest(mMockContext);
FakeFeatureFactory factory = when(mFeatureFactory.searchFeatureProvider.getExecutorService()).thenReturn(mService);
(FakeFeatureFactory) FakeFeatureFactory.getFactory(mMockContext); when(mFeatureFactory.searchFeatureProvider.getSiteMapManager())
when(factory.searchFeatureProvider.getSiteMapManager())
.thenReturn(mSiteMapManager); .thenReturn(mSiteMapManager);
mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase();
setUpDb(); setUpDb();
@@ -81,159 +100,252 @@ public class DatabaseResultLoaderTest {
} }
@Test @Test
public void testMatchTitle() { public void testMatchTitle() throws Exception {
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "title", mSiteMapManager); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "title",
assertThat(loader.loadInBackground().size()).isEqualTo(2); mSiteMapManager);
assertThat(loader.call()).hasSize(2);
verify(mSiteMapManager, times(2)).buildBreadCrumb(eq(mContext), anyString(), anyString()); verify(mSiteMapManager, times(2)).buildBreadCrumb(eq(mContext), anyString(), anyString());
} }
@Test @Test
public void testMatchSummary() { public void testMatchSummary() throws Exception {
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "summary", StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "summary",
mSiteMapManager); mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
assertThat(loader.call()).hasSize(2);
} }
@Test @Test
public void testMatchKeywords() { public void testMatchKeywords() throws Exception {
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "keywords", StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "keywords",
mSiteMapManager); mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
assertThat(loader.call()).hasSize(2);
} }
@Test @Test
public void testMatchEntries() { public void testMatchEntries() throws Exception {
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "entries", StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "entries",
mSiteMapManager); mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
assertThat(loader.call()).hasSize(2);
} }
@Test @Test
public void testSpecialCaseWord_matchesNonPrefix() { public void testSpecialCaseWord_matchesNonPrefix() throws Exception {
insertSpecialCase("Data usage"); insertSpecialCase("Data usage");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "usage", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "usage",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseDash_matchesWordNoDash() { public void testSpecialCaseDash_matchesWordNoDash() throws Exception {
insertSpecialCase("wi-fi calling"); insertSpecialCase("wi-fi calling");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "wifi", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "wifi",
public void testSpecialCaseDash_matchesWordWithDash() {
insertSpecialCase("priorités seulment");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "priorités",
mSiteMapManager); mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseDash_matchesWordWithoutDash() { public void testSpecialCaseDash_matchesWordWithDash() throws Exception {
insertSpecialCase("priorités seulment"); insertSpecialCase("priorités seulment");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "priorites",
StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "priorités",
mSiteMapManager); mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseDash_matchesEntireQueryWithoutDash() { public void testSpecialCaseDash_matchesWordWithoutDash() throws Exception {
insertSpecialCase("priorités seulment");
StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "priorites",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
}
@Test
public void testSpecialCaseDash_matchesEntireQueryWithoutDash() throws Exception {
insertSpecialCase("wi-fi calling"); insertSpecialCase("wi-fi calling");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "wifi calling",
StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "wifi calling",
mSiteMapManager); mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCasePrefix_matchesPrefixOfEntry() { public void testSpecialCasePrefix_matchesPrefixOfEntry() throws Exception {
insertSpecialCase("Photos"); insertSpecialCase("Photos");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "pho", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "pho",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() { public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() throws Exception {
insertSpecialCase("Photos"); insertSpecialCase("Photos");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "hot", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(0); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "hot",
mSiteMapManager);
assertThat(loader.call()).hasSize(0);
} }
@Test @Test
public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() { public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() throws Exception {
insertSpecialCase("Apps Notifications"); insertSpecialCase("Apps Notifications");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "Apps", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "Apps",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() { public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() throws Exception {
insertSpecialCase("Apps Notifications"); insertSpecialCase("Apps Notifications");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "Not", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "Not",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry() { public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry()
throws Exception {
insertSpecialCase("Apps Notifications"); insertSpecialCase("Apps Notifications");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "pp", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(0); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "pp",
mSiteMapManager);
assertThat(loader.call()).hasSize(0);
} }
@Test @Test
public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry() { public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry()
throws Exception {
insertSpecialCase("Apps Notifications"); insertSpecialCase("Apps Notifications");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "tion", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(0); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "tion",
mSiteMapManager);
assertThat(loader.call()).hasSize(0);
} }
@Test @Test
public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() { public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() throws
Exception {
insertSpecialCase("Apps & Notifications"); insertSpecialCase("Apps & Notifications");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "App", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "App",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry() { public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry()
throws Exception {
insertSpecialCase("Apps & Notifications"); insertSpecialCase("Apps & Notifications");
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "No", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "No",
mSiteMapManager);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testResultMatchedByMultipleQueries_duplicatesRemoved() { public void testResultMatchedByMultipleQueries_duplicatesRemoved() throws Exception {
String key = "durr"; String key = "durr";
insertSameValueAllFieldsCase(key); insertSameValueAllFieldsCase(key);
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, key, null);
assertThat(loader.loadInBackground().size()).isEqualTo(1); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, key, null);
assertThat(loader.call()).hasSize(1);
} }
@Test @Test
public void testSpecialCaseTwoWords_multipleResults() { public void testSpecialCaseTwoWords_multipleResults() throws Exception {
final String caseOne = "Apple pear"; final String caseOne = "Apple pear";
final String caseTwo = "Banana apple"; final String caseTwo = "Banana apple";
insertSpecialCase(caseOne); insertSpecialCase(caseOne);
insertSpecialCase(caseTwo); insertSpecialCase(caseTwo);
DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "App", null); StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "App", null);
Set<? extends SearchResult> results = loader.loadInBackground();
Set<CharSequence> expectedTitles = new HashSet<>(Arrays.asList(caseOne, caseTwo)); List<? extends SearchResult> results = loader.call();
Set<CharSequence> actualTitles = new HashSet<>();
Set<String> actualTitles = new HashSet<>();
for (SearchResult result : results) { for (SearchResult result : results) {
actualTitles.add(result.title); actualTitles.add(result.title.toString());
} }
assertThat(actualTitles).isEqualTo(expectedTitles); assertThat(actualTitles).containsAllOf(caseOne, caseTwo);
}
@Test
public void testGetRankingScoreByStableId_sortedDynamically() throws Exception {
FutureTask<List<Pair<String, Float>>> task = mock(FutureTask.class);
when(task.get(anyLong(), any(TimeUnit.class))).thenReturn(getDummyRankingScores());
when(mFeatureFactory.searchFeatureProvider.getRankerTask(any(Context.class),
anyString())).thenReturn(task);
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(
true);
insertSpecialCase(STATIC_TITLES[0], STABLE_IDS[0]);
insertSpecialCase(STATIC_TITLES[1], STABLE_IDS[1]);
insertSpecialCase(STATIC_TITLES[2], STABLE_IDS[2]);
StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "Static",
null);
List<? extends SearchResult> results = loader.call();
assertThat(results.get(0).title).isEqualTo(STATIC_TITLES[2]);
assertThat(results.get(1).title).isEqualTo(STATIC_TITLES[0]);
assertThat(results.get(2).title).isEqualTo(STATIC_TITLES[1]);
}
@Test
public void testGetRankingScoreByStableId_scoresTimeout_sortedStatically() throws Exception {
Callable<List<Pair<String, Float>>> callable = mock(Callable.class);
when(callable.call()).thenThrow(new TimeoutException());
FutureTask<List<Pair<String, Float>>> task = new FutureTask<>(callable);
when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(
true);
when(mFeatureFactory.searchFeatureProvider.getRankerTask(any(Context.class),
anyString())).thenReturn(task);
insertSpecialCase("title", STABLE_IDS[0]);
StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "title", null);
List<? extends SearchResult> results = loader.call();
assertThat(results.get(0).title).isEqualTo("title");
assertThat(results.get(1).title).isEqualTo("alpha_title");
assertThat(results.get(2).title).isEqualTo("bravo_title");
} }
private void insertSpecialCase(String specialCase) { private void insertSpecialCase(String specialCase) {
insertSpecialCase(specialCase, specialCase.hashCode());
}
private void insertSpecialCase(String specialCase, int docId) {
String normalized = IndexData.normalizeHyphen(specialCase); String normalized = IndexData.normalizeHyphen(specialCase);
normalized = IndexData.normalizeString(normalized); normalized = IndexData.normalizeString(normalized);
final ResultPayload payload = new ResultPayload(new Intent()); final ResultPayload payload = new ResultPayload(new Intent());
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(IndexDatabaseHelper.IndexColumns.DOCID, normalized.hashCode()); values.put(IndexDatabaseHelper.IndexColumns.DOCID, docId);
values.put(IndexDatabaseHelper.IndexColumns.LOCALE, "en-us"); values.put(IndexDatabaseHelper.IndexColumns.LOCALE, "en-us");
values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, 1); values.put(IndexDatabaseHelper.IndexColumns.DATA_RANK, 1);
values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase); values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase);
@@ -373,4 +485,33 @@ public class DatabaseResultLoaderTest {
mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values);
} }
private List<? extends SearchResult> getDummyDbResults() {
List<SearchResult> results = new ArrayList<>();
ResultPayload payload = new ResultPayload(new Intent());
SearchResult.Builder builder = new SearchResult.Builder();
builder.setPayload(payload)
.setTitle(STATIC_TITLES[0])
.setStableId(STABLE_IDS[0]);
results.add(builder.build());
builder.setTitle(STATIC_TITLES[1])
.setStableId(STABLE_IDS[1]);
results.add(builder.build());
builder.setTitle(STATIC_TITLES[2])
.setStableId(STABLE_IDS[2]);
results.add(builder.build());
return results;
}
private List<Pair<String, Float>> getDummyRankingScores() {
List<? extends SearchResult> results = getDummyDbResults();
List<Pair<String, Float>> scores = new ArrayList<>();
scores.add(new Pair<>(Long.toString(results.get(2).stableId), 0.9f)); // static_three
scores.add(new Pair<>(Long.toString(results.get(0).stableId), 0.8f)); // static_one
scores.add(new Pair<>(Long.toString(results.get(1).stableId), 0.2f)); // static_two
return scores;
}
} }