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