Merge "Move search querying into a single API"

This commit is contained in:
TreeHugger Robot
2017-10-31 20:42:33 +00:00
committed by Android (Google) Code Review
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;
}
} }