From fb772248b153dcf7e940a0d0705ca38b421fd8f8 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Thu, 5 Oct 2017 16:13:36 -0700 Subject: [PATCH] Move search querying into a single API Settings now collects search results from a single loader which fetches from an aggregator. This is to facilitate the separation of search functionalitiy, where "query" becomes a single synchronous call. In this case, the aggregator will move to the unbundled app and would be called on the other end of the Query call. i.e. the new search result loader will just call query, and unbundled search will handle everything else. An important implication is that the results will be returned in a ranked order. Thus the ranking and merging logic has been moved out of the RecyclerView adapter (which is a good clean-up, anyway). The SearchResultAggregator starts a Future for each of the data sources: - Static Results - Installed Apps - Input Devices - Accessibility Services We allow up to 500ms to collect the static results, and then an additional 150ms for each subsequent loader. In my quick tests, the static results take about 20-30ms to load. The longest loader is installed apps which takes roughly 50-60ms seconds (note that this will be improved with dynamic result caching). To handle the ranking in DatabaseResultLoader, we start a Future to collect the dynamic ranking before we start the SQL queries. When the SQL is done, we wait the same timeout as before. Then we merge the results, as before. For now we have not changed how the Dynamic results are collected, but eventually they will be a cache of dynamic results. Bug: 33577327 Bug: 67360547 Test: robotests Change-Id: I91fb03f9fd059672a970f48bea21c8d655007fa3 --- .../AccessibilityServiceResultLoader.java | 169 +++--- .../search/CursorToSearchResultConverter.java | 29 +- .../search/DatabaseIndexingManager.java | 9 +- .../settings/search/DatabaseResultLoader.java | 427 ++++++++------- .../search/InputDeviceResultLoader.java | 259 +++++----- .../search/InstalledAppResultLoader.java | 251 ++++----- .../search/SearchFeatureProvider.java | 47 +- .../search/SearchFeatureProviderImpl.java | 28 +- .../settings/search/SearchFragment.java | 77 +-- .../search/SearchResultAggregator.java | 177 +++++++ .../search/SearchResultDiffCallback.java | 7 +- .../settings/search/SearchResultLoader.java | 30 ++ .../settings/search/SearchResultsAdapter.java | 340 +----------- ...ssibilityServiceResultFutureTaskTest.java} | 23 +- ...a => InputDeviceResultFutureTaskTest.java} | 28 +- .../search/InstalledAppResultLoaderTest.java | 158 +++--- .../search/MockAccessibilityLoader.java | 39 -- .../settings/search/MockAppLoader.java | 46 -- .../android/settings/search/MockDBLoader.java | 46 -- .../search/MockInputDeviceResultLoader.java | 38 -- .../search/MockSearchResultLoader.java | 29 ++ .../search/SearchFeatureProviderImplTest.java | 26 +- .../settings/search/SearchFragmentTest.java | 142 +---- .../search/SearchResultAggregatorTest.java | 271 ++++++++++ .../search/SearchResultsAdapterTest.java | 489 +----------------- ... => StaticSearchResultFutureTaskTest.java} | 283 +++++++--- 26 files changed, 1590 insertions(+), 1878 deletions(-) create mode 100644 src/com/android/settings/search/SearchResultAggregator.java create mode 100644 src/com/android/settings/search/SearchResultLoader.java rename tests/robotests/src/com/android/settings/search/{AccessibilityServiceResultLoaderTest.java => AccessibilityServiceResultFutureTaskTest.java} (83%) rename tests/robotests/src/com/android/settings/search/{InputDeviceResultLoaderTest.java => InputDeviceResultFutureTaskTest.java} (88%) delete mode 100644 tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java delete mode 100644 tests/robotests/src/com/android/settings/search/MockAppLoader.java delete mode 100644 tests/robotests/src/com/android/settings/search/MockDBLoader.java delete mode 100644 tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java create mode 100644 tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java create mode 100644 tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java rename tests/robotests/src/com/android/settings/search/{DatabaseResultLoaderTest.java => StaticSearchResultFutureTaskTest.java} (61%) diff --git a/src/com/android/settings/search/AccessibilityServiceResultLoader.java b/src/com/android/settings/search/AccessibilityServiceResultLoader.java index 345ab30577e..327aef2a355 100644 --- a/src/com/android/settings/search/AccessibilityServiceResultLoader.java +++ b/src/com/android/settings/search/AccessibilityServiceResultLoader.java @@ -30,105 +30,112 @@ import android.os.UserHandle; import android.support.annotation.VisibleForTesting; import android.support.v4.content.ContextCompat; import android.util.IconDrawableFactory; +import android.util.Log; import android.view.accessibility.AccessibilityManager; import com.android.settings.R; import com.android.settings.accessibility.AccessibilitySettings; 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.Objects; -import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; -public class AccessibilityServiceResultLoader extends AsyncLoader> { - - private static final int NAME_NO_MATCH = -1; - - private final Context mContext; - - private List mBreadcrumb; - private SiteMapManager mSiteMapManager; - @VisibleForTesting - final String mQuery; - private final AccessibilityManager mAccessibilityManager; - private final PackageManager mPackageManager; - private final int mUserId; +public class AccessibilityServiceResultLoader extends + FutureTask> { + private static final String TAG = "A11yResultFutureTask"; public AccessibilityServiceResultLoader(Context context, String query, - SiteMapManager mapManager) { - super(context); - mContext = context; - mUserId = UserHandle.myUserId(); - mSiteMapManager = mapManager; - mPackageManager = context.getPackageManager(); - mAccessibilityManager = - (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); - mQuery = query; + SiteMapManager manager) { + super(new AccessibilityServiceResultCallable(context, query, manager)); } - @Override - public Set loadInBackground() { - final Set results = new HashSet<>(); - final Context context = getContext(); - final List 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); + static class AccessibilityServiceResultCallable implements + Callable> { - results.add(new SearchResult.Builder() - .setTitle(title) - .addBreadcrumbs(getBreadCrumb()) - .setPayload(new ResultPayload(intent)) - .setRank(wordDiff) - .setIcon(icon) - .setStableId(Objects.hash(screenTitle, componentName)) - .build()); + private static final int NAME_NO_MATCH = -1; + + private final Context mContext; + private List mBreadcrumb; + private SiteMapManager mSiteMapManager; + @VisibleForTesting + final String mQuery; + 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 getBreadCrumb() { - if (mBreadcrumb == null || mBreadcrumb.isEmpty()) { - final Context context = getContext(); - mBreadcrumb = mSiteMapManager.buildBreadCrumb( - context, AccessibilitySettings.class.getName(), - context.getString(R.string.accessibility_settings)); + @Override + public List call() throws Exception { + long startTime = System.currentTimeMillis(); + final List results = new ArrayList<>(); + final List services = mAccessibilityManager + .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 result) { + private List getBreadCrumb() { + if (mBreadcrumb == null || mBreadcrumb.isEmpty()) { + mBreadcrumb = mSiteMapManager.buildBreadCrumb( + mContext, AccessibilitySettings.class.getName(), + mContext.getString(R.string.accessibility_settings)); + } + return mBreadcrumb; + } } } diff --git a/src/com/android/settings/search/CursorToSearchResultConverter.java b/src/com/android/settings/search/CursorToSearchResultConverter.java index ce64de957e3..8528c56539e 100644 --- a/src/com/android/settings/search/CursorToSearchResultConverter.java +++ b/src/com/android/settings/search/CursorToSearchResultConverter.java @@ -36,16 +36,6 @@ import java.util.Map; import java.util.Set; 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; /** @@ -62,6 +52,25 @@ public class CursorToSearchResultConverter { 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 int LONG_TITLE_LENGTH = 20; diff --git a/src/com/android/settings/search/DatabaseIndexingManager.java b/src/com/android/settings/search/DatabaseIndexingManager.java index e94befbeade..970b50f4c60 100644 --- a/src/com/android/settings/search/DatabaseIndexingManager.java +++ b/src/com/android/settings/search/DatabaseIndexingManager.java @@ -17,11 +17,13 @@ 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; -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.IndexDatabaseHelper.IndexColumns.DOCID; 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_KEYWORDS; @@ -31,7 +33,6 @@ import static com.android.settings.search.IndexDatabaseHelper.IndexColumns .DATA_SUMMARY_ON_NORMALIZED; 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.DOCID; 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.INTENT_ACTION; diff --git a/src/com/android/settings/search/DatabaseResultLoader.java b/src/com/android/settings/search/DatabaseResultLoader.java index c1663ab38bc..66548a4d8d6 100644 --- a/src/com/android/settings/search/DatabaseResultLoader.java +++ b/src/com/android/settings/search/DatabaseResultLoader.java @@ -24,35 +24,31 @@ import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.VisibleForTesting; +import android.util.Log; +import android.util.Pair; 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.List; 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> { - private static final String LOG = "DatabaseResultLoader"; +public class DatabaseResultLoader extends FutureTask> { - /* 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 static final String TAG = "DatabaseResultLoader"; public static final String[] SELECT_COLUMNS = { IndexColumns.DOCID, @@ -82,194 +78,267 @@ public class DatabaseResultLoader extends AsyncLoader result) { - // TODO Search - } + static class StaticSearchResultCallable implements + Callable> { - @Override - public Set loadInBackground() { - if (mQueryText == null || mQueryText.isEmpty()) { - return null; + public final String[] MATCH_COLUMNS_TERTIARY = { + IndexColumns.DATA_KEYWORDS, + IndexColumns.DATA_ENTRIES + }; + + @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 results = new HashSet<>(); + @Override + public List call() { + if (mQueryText == null || mQueryText.isEmpty()) { + return new ArrayList<>(); + } - results.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0])); - results.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1])); - results.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2])); - results.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3])); - return results; - } + // TODO (b/68656233) Consolidate timing metrics + long startTime = System.currentTimeMillis(); + // Start a Future to get search result scores. + FutureTask>> rankerTask = mFeatureProvider.getRankerTask( + mContext, mQueryText); - @Override - protected boolean onCancelLoad() { - // TODO - return super.onCancelLoad(); - } + if (rankerTask != null) { + ExecutorService executorService = mFeatureProvider.getExecutorService(); + executorService.execute(rankerTask); + } - /** - * 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 firstWordQuery(String[] matchColumns, int baseRank) { - final String whereClause = buildSingleWordWhereClause(matchColumns); - final String query = mQueryText + "%"; - final String[] selection = buildSingleWordSelection(query, matchColumns.length); + final Set resultSet = new HashSet<>(); - 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])); - /** - * 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 secondaryWordQuery(String[] matchColumns, int baseRank) { - final String whereClause = buildSingleWordWhereClause(matchColumns); - final String query = "% " + mQueryText + "%"; - final String[] selection = buildSingleWordSelection(query, matchColumns.length); + // Try to retrieve the scores in time. Otherwise use static ranking. + if (rankerTask != null) { + try { + final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext); + List> searchRankScores = rankerTask.get(timeoutMs, + TimeUnit.MILLISECONDS); + return getDynamicRankedResults(resultSet, searchRankScores); + } catch (TimeoutException | InterruptedException | ExecutionException e) { + Log.d(TAG, "Error waiting for result scores: " + e); + } + } - 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 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 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); + List resultList = new ArrayList<>(resultSet); + Collections.sort(resultList); + Log.i(TAG, "Static search loading took:" + (System.currentTimeMillis() - startTime)); + return resultList; } - } - /** - * Builds the SQLite WHERE clause that matches all matchColumns for a single query. - * - * @param matchColumns List of columns that will be used for matching. - * @return The constructed WHERE clause. - */ - private static String buildSingleWordWhereClause(String[] matchColumns) { - StringBuilder sb = new StringBuilder(" ("); - final int count = matchColumns.length; - for (int n = 0; n < count; n++) { - sb.append(matchColumns[n]); - sb.append(" like ? "); - if (n < count - 1) { - sb.append(" OR "); + // TODO (b/33577327) Retrieve all search results with a single query. + + /** + * 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 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); + } + + /** + * 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 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 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 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. - * - * @param matchColumns List of columns that will be used for matching. - * @return The constructed WHERE clause. - */ - private static String buildTwoWordWhereClause(String[] matchColumns) { - StringBuilder sb = new StringBuilder(" ("); - final int count = matchColumns.length; - 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 "); + /** + * Builds the SQLite WHERE clause that matches all matchColumns for a single query. + * + * @param matchColumns List of columns that will be used for matching. + * @return The constructed WHERE clause. + */ + private static String buildSingleWordWhereClause(String[] matchColumns) { + StringBuilder sb = new StringBuilder(" ("); + final int count = matchColumns.length; + for (int n = 0; n < count; n++) { + sb.append(matchColumns[n]); + sb.append(" like ? "); + 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. - * - * @param size is the number of columns to be matched. - */ - private String[] buildSingleWordSelection(String query, int size) { - String[] selection = new String[size]; - - for (int i = 0; i < size; i++) { - selection[i] = query; + /** + * Builds the SQLite WHERE clause that matches all matchColumns to two different queries. + * + * @param matchColumns List of columns that will be used for matching. + * @return The constructed WHERE clause. + */ + private static String buildTwoWordWhereClause(String[] matchColumns) { + StringBuilder sb = new StringBuilder(" ("); + final int count = matchColumns.length; + 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. - * - * @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 + "%"; + /** + * Fills out the selection array to match the query as the prefix of a single word. + * + * @param size is the number of columns to be matched. + */ + private String[] buildSingleWordSelection(String query, int size) { + String[] selection = new String[size]; - for (int i = 0; i < (size - 1); i += 2) { - selection[i] = query; - selection[i + 1] = subStringQuery; + for (int i = 0; i < size; i++) { + selection[i] = query; + } + 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 getDynamicRankedResults(Set unsortedSet, + List> searchRankScores) { + TreeSet 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> searchRankScores, int stableId) { + for (Pair 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; } } \ No newline at end of file diff --git a/src/com/android/settings/search/InputDeviceResultLoader.java b/src/com/android/settings/search/InputDeviceResultLoader.java index e5e6553a01a..598281c0b67 100644 --- a/src/com/android/settings/search/InputDeviceResultLoader.java +++ b/src/com/android/settings/search/InputDeviceResultLoader.java @@ -26,6 +26,7 @@ import android.content.pm.ServiceInfo; import android.hardware.input.InputManager; import android.hardware.input.KeyboardLayout; import android.support.annotation.VisibleForTesting; +import android.util.Log; import android.view.InputDevice; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; @@ -35,20 +36,24 @@ import com.android.settings.R; import com.android.settings.dashboard.SiteMapManager; import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; import com.android.settings.inputmethod.PhysicalKeyboardFragment; -import com.android.settings.utils.AsyncLoader; import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; 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) */ -public class InputDeviceResultLoader extends AsyncLoader> { - private static final int NAME_NO_MATCH = -1; + +public class InputDeviceResultLoader extends FutureTask> { + + private static final String TAG = "InputResultFutureTask"; @VisibleForTesting static final String PHYSICAL_KEYBOARD_FRAGMENT = PhysicalKeyboardFragment.class.getName(); @@ -56,145 +61,151 @@ public class InputDeviceResultLoader extends AsyncLoader mPhysicalKeyboardBreadcrumb; - private List 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(); + public InputDeviceResultLoader(Context context, String query, SiteMapManager manager) { + super(new InputDeviceResultCallable(context, query, manager)); } - @Override - protected void onDiscardResult(Set result) { - } + static class InputDeviceResultCallable implements + Callable> { + private static final int NAME_NO_MATCH = -1; - @Override - public Set loadInBackground() { - final Set results = new HashSet<>(); - results.addAll(buildPhysicalKeyboardSearchResults()); - results.addAll(buildVirtualKeyboardSearchResults()); - return results; - } + private final Context mContext; + private final SiteMapManager mSiteMapManager; + private final InputManager mInputManager; + private final InputMethodManager mImm; + private final PackageManager mPackageManager; + @VisibleForTesting + final String mQuery; - private Set buildPhysicalKeyboardSearchResults() { - final Set results = new HashSet<>(); - final Context context = getContext(); - final String screenTitle = context.getString(R.string.physical_keyboard_title); + private List mPhysicalKeyboardBreadcrumb; + private List mVirtualKeyboardBreadcrumb; - for (final InputDevice device : getPhysicalFullKeyboards()) { - final String deviceName = device.getName(); - final int wordDiff = InstalledAppResultLoader.getWordDifference(deviceName, mQuery); - if (wordDiff == NAME_NO_MATCH) { - continue; + public InputDeviceResultCallable(Context context, String query, SiteMapManager mapManager) { + mContext = context; + mQuery = query; + mSiteMapManager = mapManager; + mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); + mImm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE); + mPackageManager = context.getPackageManager(); + } + + @Override + public List call() { + long startTime = System.currentTimeMillis(); + final List 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 buildPhysicalKeyboardSearchResults() { + final Set 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 - .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; } - return results; - } - private Set buildVirtualKeyboardSearchResults() { - final Set results = new HashSet<>(); - final Context context = getContext(); - final String screenTitle = context.getString(R.string.add_virtual_keyboard); - final List inputMethods = mImm.getInputMethodList(); - for (InputMethodInfo info : inputMethods) { - final String title = info.loadLabel(mPackageManager).toString(); - final String summary = InputMethodAndSubtypeUtil - .getSubtypeLocaleNameListAsSentence(getAllSubtypesOf(info), context, info); - int wordDiff = InstalledAppResultLoader.getWordDifference(title, mQuery); - if (wordDiff == NAME_NO_MATCH) { - wordDiff = InstalledAppResultLoader.getWordDifference(summary, mQuery); + private Set buildVirtualKeyboardSearchResults() { + final Set results = new HashSet<>(); + final String screenTitle = mContext.getString(R.string.add_virtual_keyboard); + final List inputMethods = mImm.getInputMethodList(); + for (InputMethodInfo info : inputMethods) { + final String title = info.loadLabel(mPackageManager).toString(); + final String summary = InputMethodAndSubtypeUtil + .getSubtypeLocaleNameListAsSentence(getAllSubtypesOf(info), mContext, info); + int wordDiff = InstalledAppResultLoader.getWordDifference(title, mQuery); + if (wordDiff == NAME_NO_MATCH) { + 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) { - continue; + return results; + } + + private List 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(); - 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 mPhysicalKeyboardBreadcrumb; } - return results; - } - private List getPhysicalKeyboardBreadCrumb() { - if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) { - final Context context = getContext(); - mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb( - context, PHYSICAL_KEYBOARD_FRAGMENT, - context.getString(R.string.physical_keyboard_title)); + + private List getVirtualKeyboardBreadCrumb() { + if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) { + final Context context = mContext; + mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb( + context, VIRTUAL_KEYBOARD_FRAGMENT, + context.getString(R.string.add_virtual_keyboard)); + } + return mVirtualKeyboardBreadcrumb; } - return mPhysicalKeyboardBreadcrumb; - } - - private List getVirtualKeyboardBreadCrumb() { - if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) { - final Context context = getContext(); - mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb( - context, VIRTUAL_KEYBOARD_FRAGMENT, - context.getString(R.string.add_virtual_keyboard)); - } - return mVirtualKeyboardBreadcrumb; - } - - private List getPhysicalFullKeyboards() { - final List 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); + private List getPhysicalFullKeyboards() { + final List 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 getAllSubtypesOf(final InputMethodInfo imi) { - final int subtypeCount = imi.getSubtypeCount(); - final List allSubtypes = new ArrayList<>(subtypeCount); - for (int index = 0; index < subtypeCount; index++) { - allSubtypes.add(imi.getSubtypeAt(index)); + private static List getAllSubtypesOf(final InputMethodInfo imi) { + final int subtypeCount = imi.getSubtypeCount(); + final List allSubtypes = new ArrayList<>(subtypeCount); + for (int index = 0; index < subtypeCount; index++) { + allSubtypes.add(imi.getSubtypeAt(index)); + } + return allSubtypes; } - return allSubtypes; } } diff --git a/src/com/android/settings/search/InstalledAppResultLoader.java b/src/com/android/settings/search/InstalledAppResultLoader.java index 7645c15487c..e5d8ac1f343 100644 --- a/src/com/android/settings/search/InstalledAppResultLoader.java +++ b/src/com/android/settings/search/InstalledAppResultLoader.java @@ -29,124 +29,39 @@ import android.os.UserManager; import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import android.util.Log; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.applications.manageapplications.ManageApplications; import com.android.settings.dashboard.SiteMapManager; -import com.android.settings.utils.AsyncLoader; import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; /** * Search loader for installed apps. */ -public class InstalledAppResultLoader extends AsyncLoader> { +public class InstalledAppResultLoader extends FutureTask> { + + private static final String TAG = "InstalledAppFutureTask"; private static final int NAME_NO_MATCH = -1; private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_LAUNCHER); - private List mBreadcrumb; - private SiteMapManager mSiteMapManager; - @VisibleForTesting - final String mQuery; - private final UserManager mUserManager; - private final PackageManagerWrapper mPackageManager; - private final List 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 loadInBackground() { - final Set results = new HashSet<>(); - final PackageManager pm = mPackageManager.getPackageManager(); - - mHomeActivities.clear(); - mPackageManager.getHomeActivities(mHomeActivities); - - for (UserInfo user : getUsersToCount()) { - final List 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 - *

- * 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 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 result) { - - } - - private List getUsersToCount() { - return mUserManager.getProfiles(UserHandle.myUserId()); + public InstalledAppResultLoader(Context context, PackageManagerWrapper wrapper, + String query, SiteMapManager manager) { + super(new InstalledAppResultCallable(context, wrapper, query, manager)); } /** @@ -213,35 +128,133 @@ public class InstalledAppResultLoader extends AsyncLoader resolveInfos, String pkg) { - for (ResolveInfo info : resolveInfos) { - if (TextUtils.equals(info.activityInfo.packageName, pkg)) { + static class InstalledAppResultCallable implements + Callable> { + + private final Context mContext; + private List mBreadcrumb; + private SiteMapManager mSiteMapManager; + @VisibleForTesting + final String mQuery; + private final UserManager mUserManager; + private final PackageManagerWrapper mPackageManager; + private final List 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 call() throws Exception { + long startTime = System.currentTimeMillis(); + final List results = new ArrayList<>(); + final PackageManager pm = mPackageManager.getPackageManager(); + + mHomeActivities.clear(); + mPackageManager.getHomeActivities(mHomeActivities); + + for (UserInfo user : getUsersToCount()) { + final List 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 + *

+ * 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 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 getBreadCrumb() { - if (mBreadcrumb == null || mBreadcrumb.isEmpty()) { - final Context context = getContext(); - mBreadcrumb = mSiteMapManager.buildBreadCrumb( - context, ManageApplications.class.getName(), - context.getString(R.string.applications_settings)); + private List getUsersToCount() { + return mUserManager.getProfiles(UserHandle.myUserId()); } - 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; + private boolean isPackageInList(List resolveInfos, String pkg) { + for (ResolveInfo info : resolveInfos) { + if (TextUtils.equals(info.activityInfo.packageName, pkg)) { + return true; + } + } + return false; + } + + private List 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; } } diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java index 4df8203d796..42afee929c8 100644 --- a/src/com/android/settings/search/SearchFeatureProvider.java +++ b/src/com/android/settings/search/SearchFeatureProvider.java @@ -19,10 +19,14 @@ package com.android.settings.search; import android.annotation.NonNull; import android.content.ComponentName; import android.content.Context; +import android.util.Pair; import android.view.View; 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 @@ -43,26 +47,31 @@ public interface SearchFeatureProvider { void verifyLaunchSearchResultPageCaller(Context context, @NonNull ComponentName caller) 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. */ - DatabaseResultLoader getDatabaseSearchLoader(Context context, String query); + DatabaseResultLoader getStaticSearchResultTask(Context context, String query); /** * 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. */ - AccessibilityServiceResultLoader getAccessibilityServiceResultLoader(Context context, + AccessibilityServiceResultLoader getAccessibilityServiceResultTask(Context context, String query); /** * 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. @@ -95,6 +104,11 @@ public interface SearchFeatureProvider { */ 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. */ @@ -114,23 +128,6 @@ public interface SearchFeatureProvider { 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. * @@ -161,4 +158,10 @@ public interface SearchFeatureProvider { default void searchRankingWarmup(Context context) { } + /** + * Return a FutureTask to get a list of scores for search results. + */ + default FutureTask>> getRankerTask(Context context, String query) { + return null; + } } diff --git a/src/com/android/settings/search/SearchFeatureProviderImpl.java b/src/com/android/settings/search/SearchFeatureProviderImpl.java index af7f1777d65..e0fbfd7b44f 100644 --- a/src/com/android/settings/search/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search/SearchFeatureProviderImpl.java @@ -22,12 +22,15 @@ import android.content.Context; import android.text.TextUtils; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.dashboard.SiteMapManager; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.indexing.IndexData; import com.android.settingslib.wrapper.PackageManagerWrapper; import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** * FeatureProvider for the refactored search code. @@ -40,6 +43,7 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { private DatabaseIndexingManager mDatabaseIndexingManager; private SiteMapManager mSiteMapManager; + private ExecutorService mExecutorService; @Override public boolean isEnabled(Context context) { @@ -59,26 +63,31 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { } @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()); } @Override - public InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query) { + public InstalledAppResultLoader getInstalledAppSearchTask(Context context, String query) { return new InstalledAppResultLoader( context, new PackageManagerWrapper(context.getPackageManager()), cleanQuery(query), getSiteMapManager()); } @Override - public AccessibilityServiceResultLoader getAccessibilityServiceResultLoader(Context context, + public AccessibilityServiceResultLoader getAccessibilityServiceResultTask(Context context, String query) { return new AccessibilityServiceResultLoader(context, cleanQuery(query), getSiteMapManager()); } @Override - public InputDeviceResultLoader getInputDeviceResultLoader(Context context, String query) { + public InputDeviceResultLoader getInputDeviceResultTask(Context context, String query) { return new InputDeviceResultLoader(context, cleanQuery(query), getSiteMapManager()); } @@ -124,12 +133,21 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { .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. * * @return the cleaned query string */ - private String cleanQuery(String query) { + @VisibleForTesting + String cleanQuery(String query) { if (TextUtils.isEmpty(query)) { return null; } diff --git a/src/com/android/settings/search/SearchFragment.java b/src/com/android/settings/search/SearchFragment.java index ca951c67e52..e6316a84786 100644 --- a/src/com/android/settings/search/SearchFragment.java +++ b/src/com/android/settings/search/SearchFragment.java @@ -54,8 +54,6 @@ import com.android.settings.widget.ActionBarShadowController; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; /** * 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. */ public class SearchFragment extends InstrumentedFragment implements SearchView.OnQueryTextListener, - LoaderManager.LoaderCallbacks>, IndexingCallback { + LoaderManager.LoaderCallbacks>, IndexingCallback { private static final String TAG = "SearchFragment"; // State values @@ -78,23 +76,14 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O static final class SearchLoaderId { // Search Query IDs - public static final int DATABASE = 1; - public static final int INSTALLED_APPS = 2; - public static final int ACCESSIBILITY_SERVICES = 3; - public static final int INPUT_DEVICES = 4; + public static final int SEARCH_RESULT = 1; // Saved Query IDs - public static final int SAVE_QUERY_TASK = 5; - public static final int REMOVE_QUERY_TASK = 6; - public static final int SAVED_QUERIES = 7; + public static final int SAVE_QUERY_TASK = 2; + public static final int REMOVE_QUERY_TASK = 3; + public static final int SAVED_QUERIES = 4; } - - private static final int NUM_QUERY_LOADERS = 4; - - @VisibleForTesting - AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS); - @VisibleForTesting String mQuery; @@ -147,7 +136,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O setHasOptionsMenu(true); final LoaderManager loaderManager = getLoaderManager(); - mSearchAdapter = new SearchResultsAdapter(this, mSearchFeatureProvider); + mSearchAdapter = new SearchResultsAdapter(this /* fragment */); mSavedQueryController = new SavedQueryController( getContext(), loaderManager, mSearchAdapter); mSearchFeatureProvider.initFeedbackButton(); @@ -277,15 +266,11 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O if (isEmptyQuery) { final LoaderManager loaderManager = getLoaderManager(); - loaderManager.destroyLoader(SearchLoaderId.DATABASE); - loaderManager.destroyLoader(SearchLoaderId.INSTALLED_APPS); - loaderManager.destroyLoader(SearchLoaderId.ACCESSIBILITY_SERVICES); - loaderManager.destroyLoader(SearchLoaderId.INPUT_DEVICES); + loaderManager.destroyLoader(SearchLoaderId.SEARCH_RESULT); mShowingSavedQuery = true; mSavedQueryController.loadSavedQueries(); mSearchFeatureProvider.hideFeedbackButton(); } else { - mSearchAdapter.initializeSearch(mQuery); restartLoaders(); } @@ -301,35 +286,25 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O } @Override - public Loader> onCreateLoader(int id, Bundle args) { + public Loader> onCreateLoader(int id, Bundle args) { final Activity activity = getActivity(); - switch (id) { - case SearchLoaderId.DATABASE: - return mSearchFeatureProvider.getDatabaseSearchLoader(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); + switch(id) { + case SearchLoaderId.SEARCH_RESULT: + return mSearchFeatureProvider.getSearchResultLoader(activity, mQuery); default: return null; } } @Override - public void onLoadFinished(Loader> loader, - Set data) { - mSearchAdapter.addSearchResults(data, loader.getClass().getName()); - if (mUnfinishedLoadersCount.decrementAndGet() != 0) { - return; - } - mSearchAdapter.notifyResultsLoaded(); + public void onLoadFinished(Loader> loader, + List data) { + mSearchAdapter.postSearchResults(data); } @Override - public void onLoaderReset(Loader> loader) { + public void onLoaderReset(Loader> loader) { } /** @@ -344,13 +319,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O mSavedQueryController.loadSavedQueries(); } else { final LoaderManager loaderManager = getLoaderManager(); - loaderManager.initLoader(SearchLoaderId.DATABASE, null /* args */, this /* callback */); - loaderManager.initLoader( - 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 */); + loaderManager.initLoader(SearchLoaderId.SEARCH_RESULT, null /* args */, + this /* callback */); } requery(); @@ -388,15 +358,8 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O private void restartLoaders() { mShowingSavedQuery = false; final LoaderManager loaderManager = getLoaderManager(); - mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS); loaderManager.restartLoader( - SearchLoaderId.DATABASE, 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 */); + SearchLoaderId.SEARCH_RESULT, null /* args */, this /* callback */); } public String getQuery() { @@ -453,9 +416,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O taggedData.add(Pair.create( MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_RANK, resultViewHolder.getAdapterPosition())); - taggedData.add(Pair.create( - MetricsEvent.FIELD_SETTINGS_SEARCH_RESULT_ASYNC_RANKING_STATE, - mSearchAdapter.getAsyncRankingState())); + // TODO (b/67744820) Move metrics to SettingsIntelligence (including ranking state). taggedData.add(Pair.create( MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH, TextUtils.isEmpty(mQuery) ? 0 : mQuery.length())); diff --git a/src/com/android/settings/search/SearchResultAggregator.java b/src/com/android/settings/search/SearchResultAggregator.java new file mode 100644 index 00000000000..890e3f0db60 --- /dev/null +++ b/src/com/android/settings/search/SearchResultAggregator.java @@ -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 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> resultsArray = new SparseArray<>(); + List 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 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 mergeSearchResults( + SparseArray> resultsArray) { + List staticResults = resultsArray.get( + ResultLoaderId.STATIC_RESULTS); + List installedAppResults = resultsArray.get( + ResultLoaderId.INSTALLED_RESULTS); + List accessibilityResults = resultsArray.get( + ResultLoaderId.ACCESSIBILITY_RESULTS); + List inputDeviceResults = resultsArray.get( + ResultLoaderId.INPUT_RESULTS); + List 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; + } +} diff --git a/src/com/android/settings/search/SearchResultDiffCallback.java b/src/com/android/settings/search/SearchResultDiffCallback.java index b7bbc668952..0f0b9778c13 100644 --- a/src/com/android/settings/search/SearchResultDiffCallback.java +++ b/src/com/android/settings/search/SearchResultDiffCallback.java @@ -26,10 +26,11 @@ import java.util.List; */ public class SearchResultDiffCallback extends DiffUtil.Callback { - private List mOldList; - private List mNewList; + private List mOldList; + private List mNewList; - public SearchResultDiffCallback(List oldList, List newList) { + public SearchResultDiffCallback(List oldList, + List newList) { mOldList = oldList; mNewList = newList; } diff --git a/src/com/android/settings/search/SearchResultLoader.java b/src/com/android/settings/search/SearchResultLoader.java new file mode 100644 index 00000000000..7ec3146171a --- /dev/null +++ b/src/com/android/settings/search/SearchResultLoader.java @@ -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> { + + private final String mQuery; + + public SearchResultLoader(Context context, String query) { + super(context); + mQuery = query; + } + + @Override + public List loadInBackground() { + SearchResultAggregator aggregator = SearchResultAggregator.getInstance(); + return aggregator.fetchResults(getContext(), mQuery); + } + + @Override + protected void onDiscardResult(List result) { + } +} diff --git a/src/com/android/settings/search/SearchResultsAdapter.java b/src/com/android/settings/search/SearchResultsAdapter.java index 5fedc523583..c05ce184130 100644 --- a/src/com/android/settings/search/SearchResultsAdapter.java +++ b/src/com/android/settings/search/SearchResultsAdapter.java @@ -18,87 +18,25 @@ package com.android.settings.search; 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.widget.RecyclerView; -import android.util.ArrayMap; -import android.util.Log; -import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; 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.Collections; -import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -public class SearchResultsAdapter extends RecyclerView.Adapter - 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; +public class SearchResultsAdapter extends RecyclerView.Adapter { private final SearchFragment mFragment; - private final Context mContext; private final List mSearchResults; - private final List mStaticallyRankedSearchResults; - private Map> mResultsMap; - private final SearchFeatureProvider mSearchFeatureProvider; - private List> mSearchRankingScores; - private Handler mHandler; - private boolean mSearchResultsLoaded; - private boolean mSearchResultsUpdated; - @IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT}) - @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) { + public SearchResultsAdapter(SearchFragment fragment) { mFragment = fragment; - mContext = fragment.getContext().getApplicationContext(); mSearchResults = new ArrayList<>(); - mResultsMap = new ArrayMap<>(); - mSearchRankingScores = new ArrayList<>(); - mStaticallyRankedSearchResults = new ArrayList<>(); - mSearchFeatureProvider = searchFeatureProvider; setHasStableIds(true); } @@ -149,298 +87,30 @@ public class SearchResultsAdapter extends RecyclerView.Adapter return mSearchResults.size(); } - @MainThread - @Override - public void onRankingScoresAvailable(List> 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 results, String loaderClassName) { - if (results == null) { - return; - } - mResultsMap.put(loaderClassName, results); - } - /** * Displays recent searched queries. - * - * @return The number of saved queries to display */ - public int displaySavedQuery(List data) { + public void displaySavedQuery(List data) { clearResults(); mSearchResults.addAll(data); 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() { mSearchResults.clear(); - mStaticallyRankedSearchResults.clear(); - mResultsMap.clear(); notifyDataSetChanged(); } - @VisibleForTesting public List getSearchResults() { return mSearchResults; } - @MainThread - 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 databaseResults = - getSortedLoadedResults(DB_RESULTS_LOADER_KEY); - List installedAppResults = - getSortedLoadedResults(APP_RESULTS_LOADER_KEY); - List accessibilityResults = - getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY); - List 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 doAsyncRanking() { - Set databaseResults = - getUnsortedLoadedResults(DB_RESULTS_LOADER_KEY); - List installedAppResults = - getSortedLoadedResults(APP_RESULTS_LOADER_KEY); - List accessibilityResults = - getSortedLoadedResults(ACCESSIBILITY_LOADER_KEY); - List inputDeviceResults = - getSortedLoadedResults(INPUT_DEVICE_LOADER_KEY); - int dbSize = databaseResults.size(); - int appSize = installedAppResults.size(); - int a11ySize = accessibilityResults.size(); - int inputDeviceSize = inputDeviceResults.size(); - - final List asyncRankingResults = new ArrayList<>( - dbSize + appSize + a11ySize + inputDeviceSize); - TreeSet dbResultsSortedByScores = new TreeSet<>( - new Comparator() { - @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 getUnsortedLoadedResults(String loaderKey) { - return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<>(); - } - - @VisibleForTesting - List getSortedLoadedResults(String loaderKey) { - List 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 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 newSearchResults, boolean detectMoves) { + public void postSearchResults(List newSearchResults) { final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( - new SearchResultDiffCallback(mSearchResults, newSearchResults), detectMoves); + new SearchResultDiffCallback(mSearchResults, newSearchResults)); mSearchResults.clear(); mSearchResults.addAll(newSearchResults); diffResult.dispatchUpdatesTo(this); mFragment.onSearchResultsDisplayed(mSearchResults.size()); - mSearchResultsUpdated = true; } } diff --git a/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultFutureTaskTest.java similarity index 83% rename from tests/robotests/src/com/android/settings/search/AccessibilityServiceResultLoaderTest.java rename to tests/robotests/src/com/android/settings/search/AccessibilityServiceResultFutureTaskTest.java index 4896dc48dd4..0e4abe18adf 100644 --- a/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search/AccessibilityServiceResultFutureTaskTest.java @@ -17,6 +17,7 @@ package com.android.settings.search; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -46,7 +47,7 @@ import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) -public class AccessibilityServiceResultLoaderTest { +public class AccessibilityServiceResultFutureTaskTest { private static final String QUERY = "test_query"; @@ -59,7 +60,7 @@ public class AccessibilityServiceResultLoaderTest { @Mock private SiteMapManager mSiteMapManager; - private AccessibilityServiceResultLoader mLoader; + private AccessibilityServiceResultLoader.AccessibilityServiceResultCallable mCallable; @Before public void setUp() { @@ -68,19 +69,20 @@ public class AccessibilityServiceResultLoaderTest { .thenReturn(mAccessibilityManager); when(mContext.getPackageManager()).thenReturn(mPackageManager); - mLoader = new AccessibilityServiceResultLoader(mContext, QUERY, mSiteMapManager); + mCallable = new AccessibilityServiceResultLoader.AccessibilityServiceResultCallable( + mContext, QUERY, mSiteMapManager); } @Test - public void query_noService_shouldNotReturnAnything() { - assertThat(mLoader.loadInBackground()).isEmpty(); + public void query_noService_shouldNotReturnAnything() throws Exception { + assertThat(mCallable.call()).isEmpty(); } @Test - public void query_hasServiceMatchingTitle_shouldReturnResult() { + public void query_hasServiceMatchingTitle_shouldReturnResult() throws Exception { addFakeAccessibilityService(); - List results = new ArrayList<>(mLoader.loadInBackground()); + List results = mCallable.call(); assertThat(results).hasSize(1); SearchResult result = results.get(0); @@ -88,13 +90,14 @@ public class AccessibilityServiceResultLoaderTest { } @Test - public void query_serviceDoesNotMatchTitle_shouldReturnResult() { + public void query_serviceDoesNotMatchTitle_shouldReturnResult() throws Exception { addFakeAccessibilityService(); - mLoader = new AccessibilityServiceResultLoader(mContext, + mCallable = new AccessibilityServiceResultLoader.AccessibilityServiceResultCallable( + mContext, QUERY + "no_match", mSiteMapManager); - assertThat(mLoader.loadInBackground()).isEmpty(); + assertThat(mCallable.call()).isEmpty(); } private void addFakeAccessibilityService() { diff --git a/tests/robotests/src/com/android/settings/search/InputDeviceResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/InputDeviceResultFutureTaskTest.java similarity index 88% rename from tests/robotests/src/com/android/settings/search/InputDeviceResultLoaderTest.java rename to tests/robotests/src/com/android/settings/search/InputDeviceResultFutureTaskTest.java index a955af179ff..e31b3d76997 100644 --- a/tests/robotests/src/com/android/settings/search/InputDeviceResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search/InputDeviceResultFutureTaskTest.java @@ -17,9 +17,11 @@ package com.android.settings.search; 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.VIRTUAL_KEYBOARD_FRAGMENT; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verifyZeroInteractions; @@ -58,7 +60,7 @@ import java.util.List; shadows = { ShadowInputDevice.class }) -public class InputDeviceResultLoaderTest { +public class InputDeviceResultFutureTaskTest { private static final String QUERY = "test_query"; private static final List PHYSICAL_KEYBOARD_BREADCRUMB; @@ -84,7 +86,7 @@ public class InputDeviceResultLoaderTest { @Mock private PackageManager mPackageManager; - private InputDeviceResultLoader mLoader; + private InputDeviceResultLoader.InputDeviceResultCallable mCallable; @Before public void setUp() { @@ -99,7 +101,8 @@ public class InputDeviceResultLoaderTest { when(mContext.getString(anyInt())) .thenAnswer(invocation -> RuntimeEnvironment.application.getString( (Integer) invocation.getArguments()[0])); - mLoader = new InputDeviceResultLoader(mContext, QUERY, mSiteMapManager); + mCallable = new InputDeviceResultLoader.InputDeviceResultCallable(mContext, QUERY, + mSiteMapManager); } @After @@ -108,18 +111,19 @@ public class InputDeviceResultLoaderTest { } @Test - public void query_noKeyboard_shouldNotReturnAnything() { - assertThat(mLoader.loadInBackground()).isEmpty(); + public void query_noKeyboard_shouldNotReturnAnything() throws Exception { + + assertThat(mCallable.call()).isEmpty(); } @Test - public void query_hasPhysicalKeyboard_match() { + public void query_hasPhysicalKeyboard_match() throws Exception { addPhysicalKeyboard(QUERY); when(mSiteMapManager.buildBreadCrumb(mContext, PHYSICAL_KEYBOARD_FRAGMENT, RuntimeEnvironment.application.getString(R.string.physical_keyboard_title))) .thenReturn(PHYSICAL_KEYBOARD_BREADCRUMB); - final List results = new ArrayList<>(mLoader.loadInBackground()); + final List results = mCallable.call(); assertThat(results).hasSize(1); assertThat(results.get(0).title).isEqualTo(QUERY); @@ -128,13 +132,13 @@ public class InputDeviceResultLoaderTest { } @Test - public void query_hasVirtualKeyboard_match() { + public void query_hasVirtualKeyboard_match() throws Exception { addVirtualKeyboard(QUERY); when(mSiteMapManager.buildBreadCrumb(mContext, VIRTUAL_KEYBOARD_FRAGMENT, RuntimeEnvironment.application.getString(R.string.add_virtual_keyboard))) .thenReturn(VIRTUAL_KEYBOARD_BREADCRUMB); - final List results = new ArrayList<>(mLoader.loadInBackground()); + final List results = mCallable.call(); assertThat(results).hasSize(1); assertThat(results.get(0).title).isEqualTo(QUERY); assertThat(results.get(0).breadcrumbs) @@ -142,11 +146,11 @@ public class InputDeviceResultLoaderTest { } @Test - public void query_hasPhysicalVirtualKeyboard_doNotMatch() { + public void query_hasPhysicalVirtualKeyboard_doNotMatch() throws Exception { addPhysicalKeyboard("abc"); addVirtualKeyboard("def"); - assertThat(mLoader.loadInBackground()).isEmpty(); + assertThat(mCallable.call()).isEmpty(); verifyZeroInteractions(mSiteMapManager); } @@ -170,4 +174,4 @@ public class InputDeviceResultLoaderTest { when(mImm.getInputMethodList()).thenReturn(imis); } -} +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java index f1a25a12002..0e84dc74e8b 100644 --- a/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search/InstalledAppResultLoaderTest.java @@ -19,7 +19,9 @@ package com.android.settings.search; import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyList; @@ -53,8 +55,6 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.robolectric.annotation.Config; import java.util.ArrayList; @@ -77,7 +77,7 @@ public class InstalledAppResultLoaderTest { @Mock private SiteMapManager mSiteMapManager; - private InstalledAppResultLoader mLoader; + private InstalledAppResultLoader.InstalledAppResultCallable mCallable; @Before public void setUp() { @@ -109,49 +109,50 @@ public class InstalledAppResultLoaderTest { } @Test - public void query_noMatchingQuery_shouldReturnEmptyResult() { + public void query_noMatchingQuery_shouldReturnEmptyResult() throws Exception { final String query = "abc"; - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground()).isEmpty(); + assertThat(mCallable.call()).isEmpty(); } @Test - public void query_matchingQuery_shouldReturnNonSystemApps() { + public void query_matchingQuery_shouldReturnNonSystemApps() throws Exception { final String query = "app"; - mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = spy(new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager)); - when(mLoader.getContext()).thenReturn(mContext); when(mSiteMapManager.buildBreadCrumb(eq(mContext), anyString(), anyString())) .thenReturn(Arrays.asList(new String[]{"123"})); - assertThat(mLoader.loadInBackground().size()).isEqualTo(3); + assertThat(mCallable.call()).hasSize(3); verify(mSiteMapManager) .buildBreadCrumb(eq(mContext), anyString(), anyString()); } @Test - public void query_matchingQuery_shouldReturnSystemAppUpdates() { + public void query_matchingQuery_shouldReturnSystemAppUpdates() throws Exception { when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_UPDATED_SYSTEM_APP, 0 /* targetSdkVersion */))); final String query = "app"; - mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = spy(new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager)); - when(mLoader.getContext()).thenReturn(mContext); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); verify(mSiteMapManager) .buildBreadCrumb(eq(mContext), anyString(), anyString()); } @Test - public void query_matchingQuery_shouldReturnSystemAppIfLaunchable() { + public void query_matchingQuery_shouldReturnSystemAppIfLaunchable() throws Exception { when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM, @@ -164,14 +165,15 @@ public class InstalledAppResultLoaderTest { final String query = "app"; - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_matchingQuery_shouldReturnSystemAppIfHomeApp() { + public void query_matchingQuery_shouldReturnSystemAppIfHomeApp() throws Exception { when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM, @@ -180,28 +182,26 @@ public class InstalledAppResultLoaderTest { any(Intent.class), anyInt(), anyInt())) .thenReturn(null); - when(mPackageManagerWrapper.getHomeActivities(anyList())).thenAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - final List list = (List) invocation.getArguments()[0]; - final ResolveInfo info = new ResolveInfo(); - info.activityInfo = new ActivityInfo(); - info.activityInfo.packageName = "app1"; - list.add(info); - return null; - } + when(mPackageManagerWrapper.getHomeActivities(anyList())).thenAnswer(invocation -> { + final List list = (List) invocation.getArguments()[0]; + final ResolveInfo info = new ResolveInfo(); + info.activityInfo = new ActivityInfo(); + info.activityInfo.packageName = "app1"; + list.add(info); + return null; }); final String query = "app"; - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_matchingQuery_shouldNotReturnSystemAppIfNotLaunchable() { + public void query_matchingQuery_shouldNotReturnSystemAppIfNotLaunchable() throws Exception { when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM, @@ -212,21 +212,23 @@ public class InstalledAppResultLoaderTest { final String query = "app"; - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground()).isEmpty(); + assertThat(mCallable.call()).isEmpty(); verify(mSiteMapManager, never()) .buildBreadCrumb(eq(mContext), anyString(), anyString()); } @Test - public void query_matchingQuery_multipleResults() { + public void query_matchingQuery_multipleResults() throws Exception { final String query = "app"; - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - final Set results = mLoader.loadInBackground(); + final List results = mCallable.call(); Set expectedTitles = new HashSet<>(Arrays.asList("app4", "app", "appBuffer")); Set actualTitles = new HashSet<>(); @@ -237,161 +239,172 @@ public class InstalledAppResultLoaderTest { } @Test - public void query_normalWord_MatchPrefix() { + public void query_normalWord_MatchPrefix() throws Exception { final String query = "ba"; final String packageName = "Bananas"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_CapitalCase_DoestMatchSecondWord() { + public void query_CapitalCase_DoestMatchSecondWord() throws Exception { final String query = "Apples"; final String packageName = "BananasApples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(0); + assertThat(mCallable.call()).isEmpty(); } @Test - public void query_TwoWords_MatchesFirstWord() { + public void query_TwoWords_MatchesFirstWord() throws Exception { final String query = "Banana"; final String packageName = "Bananas Apples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_TwoWords_MatchesSecondWord() { + public void query_TwoWords_MatchesSecondWord() throws Exception { final String query = "Apple"; final String packageName = "Bananas Apples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_ThreeWords_MatchesThirdWord() { + public void query_ThreeWords_MatchesThirdWord() throws Exception { final String query = "Pear"; final String packageName = "Bananas Apples Pears"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_DoubleSpacedWords_MatchesSecondWord() { + public void query_DoubleSpacedWords_MatchesSecondWord() throws Exception { final String query = "Apple"; final String packageName = "Bananas Apples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_SpecialChar_MatchesSecondWord() { + public void query_SpecialChar_MatchesSecondWord() throws Exception { final String query = "Apple"; final String packageName = "Bananas & Apples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_TabSeparated_MatchesSecondWord() { + public void query_TabSeparated_MatchesSecondWord() throws Exception { final String query = "Apple"; final String packageName = "Bananas\tApples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_LeadingNumber_MatchesWord() { + public void query_LeadingNumber_MatchesWord() throws Exception { final String query = "4"; final String packageName = "4Bananas"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(1); + assertThat(mCallable.call()).hasSize(1); } @Test - public void query_FirstWordPrefixOfQuery_NoMatch() { + public void query_FirstWordPrefixOfQuery_NoMatch() throws Exception { final String query = "Bananass"; final String packageName = "Bananas Apples"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(0); + assertThat(mCallable.call()).isEmpty(); } @Test - public void query_QueryLongerThanAppName_NoMatch() { + public void query_QueryLongerThanAppName_NoMatch() throws Exception { final String query = "BananasApples"; final String packageName = "Bananas"; when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt())) .thenReturn(Arrays.asList( ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - assertThat(mLoader.loadInBackground().size()).isEqualTo(0); + assertThat(mCallable.call()).isEmpty(); } @Test - public void query_appExistsInBothProfiles() { + public void query_appExistsInBothProfiles() throws Exception { final String query = "carrot"; final String packageName = "carrot"; final int user1 = 0; @@ -414,10 +427,11 @@ public class InstalledAppResultLoaderTest { packageName, 0 /* flags */, 0 /* targetSdkVersion */))); - mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mCallable = new InstalledAppResultLoader.InstalledAppResultCallable(mContext, + mPackageManagerWrapper, query, mSiteMapManager); - Set searchResults = (Set) mLoader.loadInBackground(); + List searchResults = (List) mCallable.call(); assertThat(searchResults).hasSize(2); Set uidResults = searchResults.stream().map(result -> result.info.uid).collect( diff --git a/tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java b/tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java deleted file mode 100644 index 0a06a351915..00000000000 --- a/tests/robotests/src/com/android/settings/search/MockAccessibilityLoader.java +++ /dev/null @@ -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 loadInBackground() { - return new HashSet<>(); - } - - @Override - protected void onDiscardResult(Set result) { - - } -} diff --git a/tests/robotests/src/com/android/settings/search/MockAppLoader.java b/tests/robotests/src/com/android/settings/search/MockAppLoader.java deleted file mode 100644 index c68cbdf623d..00000000000 --- a/tests/robotests/src/com/android/settings/search/MockAppLoader.java +++ /dev/null @@ -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 loadInBackground() { - return new HashSet<>(); - } - - @Override - protected void onDiscardResult(Set result) { - - } -} diff --git a/tests/robotests/src/com/android/settings/search/MockDBLoader.java b/tests/robotests/src/com/android/settings/search/MockDBLoader.java deleted file mode 100644 index b28c1ed97f6..00000000000 --- a/tests/robotests/src/com/android/settings/search/MockDBLoader.java +++ /dev/null @@ -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 loadInBackground() { - return new HashSet<>(); - } - - @Override - protected void onDiscardResult(Set result) { - - } -} diff --git a/tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java b/tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java deleted file mode 100644 index 2c16b14a3b0..00000000000 --- a/tests/robotests/src/com/android/settings/search/MockInputDeviceResultLoader.java +++ /dev/null @@ -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 loadInBackground() { - return new HashSet<>(); - } - - @Override - protected void onDiscardResult(Set result) { - - } -} diff --git a/tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java b/tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java new file mode 100644 index 00000000000..6af258ab1de --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/MockSearchResultLoader.java @@ -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 loadInBackground() { + return new ArrayList<>(); + } + + @Override + protected void onDiscardResult(List result) { + } +} diff --git a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java index 050d7aa5bbe..a529b0baf52 100644 --- a/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchFeatureProviderImplTest.java @@ -21,7 +21,6 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Activity; import android.content.ComponentName; - import com.android.settings.TestConfig; import com.android.settings.dashboard.SiteMapManager; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -33,6 +32,10 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; 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) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class SearchFeatureProviderImplTest { @@ -43,7 +46,7 @@ public class SearchFeatureProviderImplTest { public void setUp() { MockitoAnnotations.initMocks(this); mActivity = Robolectric.buildActivity(Activity.class).create().visible().get(); - mProvider = new SearchFeatureProviderImpl(); + mProvider = spy(new SearchFeatureProviderImpl()); } @Test @@ -57,18 +60,19 @@ public class SearchFeatureProviderImplTest { @Test public void getDatabaseSearchLoader_shouldCleanupQuery() { 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 public void getInstalledAppSearchLoader_shouldCleanupQuery() { 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) @@ -87,4 +91,12 @@ public class SearchFeatureProviderImplTest { final ComponentName cn = new ComponentName(mActivity.getPackageName(), "class"); mProvider.verifyLaunchSearchResultPageCaller(mActivity, cn); } + + @Test + public void cleanQuery_trimsWhitespace() { + final String query = " space "; + final String cleanQuery = "space"; + + assertThat(mProvider.cleanQuery(query)).isEqualTo(cleanQuery); + } } diff --git a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java index b897008aa80..a3f33343209 100644 --- a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java @@ -18,6 +18,7 @@ package com.android.settings.search; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; @@ -54,9 +55,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; -import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatcher; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; @@ -65,7 +64,7 @@ import org.robolectric.android.controller.ActivityController; import org.robolectric.annotation.Config; import org.robolectric.util.ReflectionHelpers; -import java.util.Set; +import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, @@ -79,22 +78,13 @@ public class SearchFragmentTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; @Mock - private DatabaseResultLoader mDatabaseResultLoader; - @Mock - private InstalledAppResultLoader mInstalledAppResultLoader; - @Mock - private AccessibilityServiceResultLoader mAccessibilityServiceResultLoader; - @Mock - private InputDeviceResultLoader mInputDeviceResultLoader; - + private SearchResultLoader mSearchResultLoader; @Mock private SavedQueryLoader mSavedQueryLoader; @Mock private SavedQueryController mSavedQueryController; @Mock private SearchResultsAdapter mSearchResultsAdapter; - @Captor - private ArgumentCaptor mQueryCaptor = ArgumentCaptor.forClass(String.class); private FakeFeatureFactory mFeatureFactory; @@ -113,17 +103,8 @@ public class SearchFragmentTest { @Test public void screenRotate_shouldPersistQuery() { 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); + .getSearchResultLoader(any(Context.class), anyString())) + .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application)); when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) .thenReturn(mSavedQueryLoader); @@ -168,25 +149,16 @@ public class SearchFragmentTest { activityController.setup(bundle); verify(mFeatureFactory.searchFeatureProvider, never()) - .getDatabaseSearchLoader(any(Context.class), anyString()); + .getStaticSearchResultTask(any(Context.class), anyString()); verify(mFeatureFactory.searchFeatureProvider, never()) - .getInstalledAppSearchLoader(any(Context.class), anyString()); + .getInstalledAppSearchTask(any(Context.class), anyString()); } @Test - public void queryTextChange_shouldTriggerLoaderAndInitializeSearch() { + public void queryTextChange_shouldTriggerLoader() { 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); + .getSearchResultLoader(any(Context.class), anyString())) + .thenReturn(mSearchResultLoader); when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) .thenReturn(mSavedQueryLoader); @@ -199,7 +171,6 @@ public class SearchFragmentTest { when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class))) .thenReturn(true); - ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter); fragment.onQueryTextChange(testQuery); activityController.get().onBackPressed(); @@ -209,11 +180,7 @@ public class SearchFragmentTest { any(Context.class), eq(MetricsProto.MetricsEvent.ACTION_LEAVE_SEARCH_RESULT_WITHOUT_QUERY)); verify(mFeatureFactory.searchFeatureProvider) - .getDatabaseSearchLoader(any(Context.class), anyString()); - verify(mFeatureFactory.searchFeatureProvider) - .getInstalledAppSearchLoader(any(Context.class), anyString()); - verify(mSearchResultsAdapter).initializeSearch(mQueryCaptor.capture()); - assertThat(mQueryCaptor.getValue()).isEqualTo(testQuery); + .getSearchResultLoader(any(Context.class), anyString()); } @Test @@ -238,18 +205,6 @@ public class SearchFragmentTest { @Test 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))) .thenReturn(mSavedQueryLoader); ActivityController activityController = @@ -266,27 +221,14 @@ public class SearchFragmentTest { fragment.onQueryTextChange(""); verify(mFeatureFactory.searchFeatureProvider, never()) - .getDatabaseSearchLoader(any(Context.class), anyString()); + .getStaticSearchResultTask(any(Context.class), anyString()); verify(mFeatureFactory.searchFeatureProvider, never()) - .getInstalledAppSearchLoader(any(Context.class), anyString()); + .getInstalledAppSearchTask(any(Context.class), anyString()); verify(mSavedQueryController).loadSavedQueries(); - verify(mSearchResultsAdapter, never()).initializeSearch(anyString()); } @Test 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))) .thenReturn(mSavedQueryLoader); @@ -303,41 +245,11 @@ public class SearchFragmentTest { 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 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 public void whenNoQuery_HideFeedbackIsCalled() { 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)); + .getSearchResultLoader(any(Context.class), anyString())) + .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application)); when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) .thenReturn(mSavedQueryLoader); @@ -359,17 +271,8 @@ public class SearchFragmentTest { @Test public void onLoadFinished_ShowsFeedback() { 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 - .getAccessibilityServiceResultLoader(any(Context.class), anyString())) - .thenReturn(new MockAccessibilityLoader(RuntimeEnvironment.application)); - when(mFeatureFactory.searchFeatureProvider - .getInputDeviceResultLoader(any(Context.class), anyString())) - .thenReturn(new MockInputDeviceResultLoader(RuntimeEnvironment.application)); + .getSearchResultLoader(any(Context.class), anyString())) + .thenReturn(new MockSearchResultLoader(RuntimeEnvironment.application)); when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class))) .thenReturn(mSavedQueryLoader); ActivityController activityController = @@ -413,9 +316,7 @@ public class SearchFragmentTest { fragment.onIndexingFinished(); - verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.DATABASE), - eq(null), any(LoaderManager.LoaderCallbacks.class)); - verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.INSTALLED_APPS), + verify(loaderManager).initLoader(eq(SearchFragment.SearchLoaderId.SEARCH_RESULT), eq(null), any(LoaderManager.LoaderCallbacks.class)); } @@ -480,16 +381,13 @@ public class SearchFragmentTest { eq("test_setting"), 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_ASYNC_RANKING_STATE)), argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SEARCH_QUERY_LENGTH))); - verify(mFeatureFactory.searchFeatureProvider).searchResultClicked(nullable(Context.class), nullable(String.class), eq(searchResult)); } @Test - public void onResume_shouldCallSearchRankingWarmupIfSmartSearchRankingEnabled(){ + public void onResume_shouldCallSearchRankingWarmupIfSmartSearchRankingEnabled() { when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class))) .thenReturn(true); @@ -504,7 +402,7 @@ public class SearchFragmentTest { } @Test - public void onResume_shouldNotCallSearchRankingWarmupIfSmartSearchRankingDisabled(){ + public void onResume_shouldNotCallSearchRankingWarmupIfSmartSearchRankingDisabled() { when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(any(Context.class))) .thenReturn(false); diff --git a/tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java b/tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java new file mode 100644 index 00000000000..286d7cf93c3 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/SearchResultAggregatorTest.java @@ -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 dbResults = getDummyDbResults(); + doReturn(dbResults).when(mStaticTask).get(anyLong(), any(TimeUnit.class)); + + List appResults = getDummyAppResults(); + doReturn(appResults).when(mAppTask).get(anyLong(), any(TimeUnit.class)); + + List inputResults = getDummyInputDeviceResults(); + doReturn(inputResults).when(mInputTask).get(anyLong(), any(TimeUnit.class)); + + List accessResults = getDummyAccessibilityResults(); + doReturn(accessResults).when(mMAccessibilityTask).get(anyLong(), any(TimeUnit.class)); + } + + @Test + public void testStaticResults_mergedProperly() { + when(mFeatureFactory.searchFeatureProvider.isSmartSearchRankingEnabled(mContext)) + .thenReturn(false); + + List 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 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 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 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 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 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 getDummyDbResults() { + List 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 getDummyAppResults() { + List 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 getDummyInputDeviceResults() { + List 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 getDummyAccessibilityResults() { + List 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; + } +} diff --git a/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java index a2afb3570c6..4baf8d2acd6 100644 --- a/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java @@ -18,44 +18,32 @@ package com.android.settings.search; 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.anyString; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.graphics.drawable.Drawable; -import android.util.Pair; import android.view.ViewGroup; import android.widget.FrameLayout; -import com.android.settings.R; 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 org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowLooper; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; import java.util.Objects; -import java.util.Set; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) @@ -67,25 +55,18 @@ public class SearchResultsAdapterTest { private SearchFeatureProvider mSearchFeatureProvider; @Mock private Context mMockContext; - @Captor - private ArgumentCaptor mSearchResultsCountCaptor = - ArgumentCaptor.forClass(Integer.class); private SearchResultsAdapter mAdapter; private Context mContext; - private String mLoaderClassName; - - private String[] TITLES = {"alpha", "bravo", "charlie", "appAlpha", "appBravo", "appCharlie"}; @Before public void setUp() { MockitoAnnotations.initMocks(this); mContext = Robolectric.buildActivity(Activity.class).get(); - mLoaderClassName = DatabaseResultLoader.class.getName(); when(mFragment.getContext()).thenReturn(mMockContext); when(mMockContext.getApplicationContext()).thenReturn(mContext); when(mSearchFeatureProvider.smartSearchRankingTimeoutMs(any(Context.class))) .thenReturn(300L); - mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider); + mAdapter = new SearchResultsAdapter(mFragment); } @Test @@ -94,17 +75,6 @@ public class SearchResultsAdapterTest { assertThat(updatedResults).isEmpty(); } - @Test - public void testSingleSourceMerge_exactCopyReturned() { - Set intentResults = getIntentSampleResults(); - mAdapter.initializeSearch(""); - mAdapter.addSearchResults(intentResults, mLoaderClassName); - mAdapter.notifyResultsLoaded(); - - List updatedResults = mAdapter.getSearchResults(); - assertThat(updatedResults).containsAllIn(intentResults); - } - @Test public void testCreateViewHolder_returnsIntentResult() { ViewGroup group = new FrameLayout(mContext); @@ -123,387 +93,13 @@ public class SearchResultsAdapterTest { } @Test - public void testEndToEndSearch_properResultsMerged_correctOrder() { - mAdapter.initializeSearch(""); - mAdapter.addSearchResults(new HashSet<>(getDummyAppResults()), - InstalledAppResultLoader.class.getName()); - mAdapter.addSearchResults(new HashSet<>(getDummyDbResults()), - DatabaseResultLoader.class.getName()); - mAdapter.notifyResultsLoaded(); + public void testPostSearchResults_addsDataAndDisplays() { + List results = getDummyDbResults(); - List 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); - } + mAdapter.postSearchResults(results); - @Test - public void testEndToEndSearch_addResults_resultsAddedInOrder() { - List appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List dbResults = getDummyDbResults(); - mAdapter.initializeSearch(""); - mAdapter.addSearchResults( - new HashSet<>(appResults), InstalledAppResultLoader.class.getName()); - mAdapter.addSearchResults( - new HashSet<>(dbResults), DatabaseResultLoader.class.getName()); - mAdapter.notifyResultsLoaded(); - - waitUntilRankingTimesOut(); - - List 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 appResults = getDummyAppResults(); - List dbResults = getDummyDbResults(); - mAdapter.initializeSearch(""); - - waitUntilRankingTimesOut(); - - mAdapter.addSearchResults( - new HashSet<>(appResults), InstalledAppResultLoader.class.getName()); - mAdapter.addSearchResults( - new HashSet<>(dbResults), DatabaseResultLoader.class.getName()); - mAdapter.notifyResultsLoaded(); - - List 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 appResults = getDummyAppResults(); - List 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 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 appResults = getDummyAppResults(); - List dbResults = getDummyDbResults(); - mAdapter.initializeSearch(""); - mAdapter.addSearchResults( - new HashSet<>(appResults), InstalledAppResultLoader.class.getName()); - mAdapter.addSearchResults( - new HashSet<>(dbResults), DatabaseResultLoader.class.getName()); - mAdapter.notifyResultsLoaded(); - List> rankingScores = getDummyRankingScores(); - rankingScores.remove(1); // no ranking score for alpha - mAdapter.onRankingScoresAvailable(rankingScores); - List 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 appResults = getDummyAppResults(); - List dbResults = getDummyDbResults(); - mAdapter.initializeSearch(""); - mAdapter.addSearchResults( - new HashSet<>(appResults), InstalledAppResultLoader.class.getName()); - mAdapter.addSearchResults( - new HashSet<>(dbResults), DatabaseResultLoader.class.getName()); - Set expectedDbTitles = new HashSet<>( - Arrays.asList("alpha", "bravo", "charlie")); - Set expectedAppTitles = new HashSet<>( - Arrays.asList("appAlpha", "appBravo", "appCharlie")); - Set actualDbTitles = new HashSet<>(); - Set 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 appResults = getDummyAppResults(); - List dbResults = getDummyDbResults(); - mAdapter.initializeSearch(""); - mAdapter.addSearchResults( - new HashSet<>(appResults), InstalledAppResultLoader.class.getName()); - mAdapter.addSearchResults( - new HashSet<>(dbResults), DatabaseResultLoader.class.getName()); - List actualDbResults = - mAdapter.getSortedLoadedResults(SearchResultsAdapter.DB_RESULTS_LOADER_KEY); - List 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 appResults = getDummyAppResults(); - List 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 - } - } + assertThat(mAdapter.getSearchResults()).containsExactlyElementsIn(results); + verify(mFragment).onSearchResultsDisplayed(anyInt()); } private List getDummyDbResults() { @@ -511,78 +107,21 @@ public class SearchResultsAdapterTest { ResultPayload payload = new ResultPayload(new Intent()); SearchResult.Builder builder = new SearchResult.Builder(); builder.setPayload(payload) - .setTitle(TITLES[0]) + .setTitle("one") .setRank(1) - .setStableId(Objects.hash(TITLES[0], "db")); + .setStableId(Objects.hash("one", "db")); results.add(builder.build()); - builder.setTitle(TITLES[1]) + builder.setTitle("two") .setRank(3) - .setStableId(Objects.hash(TITLES[1], "db")); + .setStableId(Objects.hash("two", "db")); results.add(builder.build()); - builder.setTitle(TITLES[2]) + builder.setTitle("three") .setRank(6) - .setStableId(Objects.hash(TITLES[2], "db")); + .setStableId(Objects.hash("three", "db")); results.add(builder.build()); return results; } - - private List getDummyAppResults() { - List 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 getIntentSampleResults() { - Set sampleResults = new HashSet<>(); - ArrayList 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> getDummyRankingScores() { - List results = getDummyDbResults(); - List> 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; - } } diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/StaticSearchResultFutureTaskTest.java similarity index 61% rename from tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java rename to tests/robotests/src/com/android/settings/search/StaticSearchResultFutureTaskTest.java index dd7b7a97f9d..e285555f70f 100644 --- a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search/StaticSearchResultFutureTaskTest.java @@ -21,9 +21,11 @@ import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.sqlite.SQLiteDatabase; +import android.util.Pair; import com.android.settings.TestConfig; import com.android.settings.dashboard.SiteMapManager; +import com.android.settings.search.DatabaseResultLoader.StaticSearchResultCallable; import com.android.settings.search.indexing.IndexData; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeFeatureFactory; @@ -39,37 +41,54 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import java.util.Arrays; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; 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 org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) -public class DatabaseResultLoaderTest { +public class StaticSearchResultFutureTaskTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mMockContext; @Mock private SiteMapManager mSiteMapManager; + @Mock + private ExecutorService mService; private Context mContext; 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 public void setUp() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - FakeFeatureFactory.setupForTest(mMockContext); - FakeFeatureFactory factory = - (FakeFeatureFactory) FakeFeatureFactory.getFactory(mMockContext); - when(factory.searchFeatureProvider.getSiteMapManager()) + mFeatureFactory = FakeFeatureFactory.setupForTest(mMockContext); + when(mFeatureFactory.searchFeatureProvider.getExecutorService()).thenReturn(mService); + when(mFeatureFactory.searchFeatureProvider.getSiteMapManager()) .thenReturn(mSiteMapManager); mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); setUpDb(); @@ -81,159 +100,252 @@ public class DatabaseResultLoaderTest { } @Test - public void testMatchTitle() { - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "title", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(2); + public void testMatchTitle() throws Exception { + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "title", + mSiteMapManager); + + assertThat(loader.call()).hasSize(2); verify(mSiteMapManager, times(2)).buildBreadCrumb(eq(mContext), anyString(), anyString()); } @Test - public void testMatchSummary() { - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "summary", + public void testMatchSummary() throws Exception { + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "summary", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(2); + + assertThat(loader.call()).hasSize(2); } @Test - public void testMatchKeywords() { - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "keywords", + public void testMatchKeywords() throws Exception { + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "keywords", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(2); + + assertThat(loader.call()).hasSize(2); } @Test - public void testMatchEntries() { - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "entries", + public void testMatchEntries() throws Exception { + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "entries", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(2); + + assertThat(loader.call()).hasSize(2); } @Test - public void testSpecialCaseWord_matchesNonPrefix() { + public void testSpecialCaseWord_matchesNonPrefix() throws Exception { 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 - public void testSpecialCaseDash_matchesWordNoDash() { + public void testSpecialCaseDash_matchesWordNoDash() throws Exception { insertSpecialCase("wi-fi calling"); - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "wifi", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(1); - } - @Test - public void testSpecialCaseDash_matchesWordWithDash() { - insertSpecialCase("priorités seulment"); - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "priorités", + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "wifi", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(1); + + assertThat(loader.call()).hasSize(1); } @Test - public void testSpecialCaseDash_matchesWordWithoutDash() { + public void testSpecialCaseDash_matchesWordWithDash() throws Exception { insertSpecialCase("priorités seulment"); - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "priorites", + + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "priorités", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(1); + + assertThat(loader.call()).hasSize(1); } @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"); - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "wifi calling", + + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "wifi calling", mSiteMapManager); - assertThat(loader.loadInBackground().size()).isEqualTo(1); + + assertThat(loader.call()).hasSize(1); } @Test - public void testSpecialCasePrefix_matchesPrefixOfEntry() { + public void testSpecialCasePrefix_matchesPrefixOfEntry() throws Exception { 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 - public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() { + public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() throws Exception { 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 - public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() { + public void testSpecialCaseMultiWordPrefix_matchesPrefixOfEntry() throws Exception { 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 - public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() { + public void testSpecialCaseMultiWordPrefix_matchesSecondWordPrefixOfEntry() throws Exception { 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 - public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry() { + public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry() + throws Exception { 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 - public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry() { + public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry() + throws Exception { 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 - public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() { + public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfEntry() throws + Exception { 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 - public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry() { + public void testSpecialCaseMultiWordPrefixWithSpecial_matchesPrefixOfSecondEntry() + throws Exception { 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 - public void testResultMatchedByMultipleQueries_duplicatesRemoved() { + public void testResultMatchedByMultipleQueries_duplicatesRemoved() throws Exception { String key = "durr"; 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 - public void testSpecialCaseTwoWords_multipleResults() { + public void testSpecialCaseTwoWords_multipleResults() throws Exception { final String caseOne = "Apple pear"; final String caseTwo = "Banana apple"; insertSpecialCase(caseOne); insertSpecialCase(caseTwo); - DatabaseResultLoader loader = new DatabaseResultLoader(mContext, "App", null); - Set results = loader.loadInBackground(); - Set expectedTitles = new HashSet<>(Arrays.asList(caseOne, caseTwo)); - Set actualTitles = new HashSet<>(); + StaticSearchResultCallable loader = new StaticSearchResultCallable(mContext, "App", null); + + List results = loader.call(); + + Set actualTitles = new HashSet<>(); 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>> 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 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>> callable = mock(Callable.class); + when(callable.call()).thenThrow(new TimeoutException()); + FutureTask>> 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 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) { + insertSpecialCase(specialCase, specialCase.hashCode()); + } + + private void insertSpecialCase(String specialCase, int docId) { String normalized = IndexData.normalizeHyphen(specialCase); normalized = IndexData.normalizeString(normalized); final ResultPayload payload = new ResultPayload(new Intent()); 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.DATA_RANK, 1); values.put(IndexDatabaseHelper.IndexColumns.DATA_TITLE, specialCase); @@ -373,4 +485,33 @@ public class DatabaseResultLoaderTest { mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); } + + private List getDummyDbResults() { + List 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> getDummyRankingScores() { + List results = getDummyDbResults(); + List> 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; + } } \ No newline at end of file