diff --git a/src/com/android/settings/search/InlineSwitchViewHolder.java b/src/com/android/settings/search/InlineSwitchViewHolder.java index 162eb1a2a1c..bb8320d3b36 100644 --- a/src/com/android/settings/search/InlineSwitchViewHolder.java +++ b/src/com/android/settings/search/InlineSwitchViewHolder.java @@ -46,7 +46,7 @@ public class InlineSwitchViewHolder extends SearchViewHolder { } @Override - public void onBind(SearchFragment fragment, SearchResult result) { + public void onBind(SearchFragment fragment, final SearchResult result) { super.onBind(fragment, result); if (mContext == null) { return; @@ -57,7 +57,7 @@ public class InlineSwitchViewHolder extends SearchViewHolder { final Pair value = Pair.create( MetricsEvent.FIELD_SETTINGS_SEARCH_INLINE_RESULT_VALUE, isChecked ? 1L : 0L); - fragment.onSearchResultClicked(this, payload.mSettingKey, value); + fragment.onSearchResultClicked(this, result, value); int newValue = isChecked ? InlineSwitchPayload.TRUE : InlineSwitchPayload.FALSE; payload.setValue(mContext, newValue); }); diff --git a/src/com/android/settings/search/IntentSearchViewHolder.java b/src/com/android/settings/search/IntentSearchViewHolder.java index 17ff9815c3c..11adaefc400 100644 --- a/src/com/android/settings/search/IntentSearchViewHolder.java +++ b/src/com/android/settings/search/IntentSearchViewHolder.java @@ -16,13 +16,9 @@ */ package com.android.settings.search; -import android.content.ComponentName; -import android.content.Intent; -import android.text.TextUtils; import android.view.View; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.settings.SettingsActivity; /** * ViewHolder for intent based search results. @@ -44,14 +40,8 @@ public class IntentSearchViewHolder extends SearchViewHolder { super.onBind(fragment, result); itemView.setOnClickListener(v -> { - final Intent intent = result.payload.getIntent(); - final ComponentName cn = intent.getComponent(); - String resultName = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT); - if (TextUtils.isEmpty(resultName) && cn != null) { - resultName = cn.flattenToString(); - } - fragment.onSearchResultClicked(this, resultName); - fragment.startActivity(intent); + fragment.onSearchResultClicked(this, result); + fragment.startActivity(result.payload.getIntent()); }); } } diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java index 67d4ef134de..50edae73e58 100644 --- a/src/com/android/settings/search/SearchFeatureProvider.java +++ b/src/com/android/settings/search/SearchFeatureProvider.java @@ -22,6 +22,7 @@ import android.view.Menu; import android.view.View; import com.android.settings.dashboard.SiteMapManager; +import com.android.settings.search.ranking.SearchResultsRankerCallback; import java.util.List; @@ -98,21 +99,30 @@ public interface SearchFeatureProvider { } /** - * Ranks search results based on the input query. + * Query search results based on the input query. * + * @param context application context * @param query input user query - * @param searchResults list of search results to be ranked + * @param searchResultsRankerCallback {@link SearchResultsRankerCallback} */ - default void rankSearchResults(String query, List searchResults) { + 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. * + * @param context application context * @param query input user query * @param searchResult clicked result */ - default void searchResultClicked(String query, SearchResult searchResult) { + default void searchResultClicked(Context context, String query, SearchResult searchResult) { } /** diff --git a/src/com/android/settings/search/SearchFragment.java b/src/com/android/settings/search/SearchFragment.java index 6b07f2d7079..87df62ec85f 100644 --- a/src/com/android/settings/search/SearchFragment.java +++ b/src/com/android/settings/search/SearchFragment.java @@ -19,7 +19,9 @@ package com.android.settings.search; import android.app.Activity; import android.app.LoaderManager; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.Loader; import android.os.Bundle; import android.support.annotation.VisibleForTesting; @@ -37,6 +39,7 @@ import android.widget.SearchView; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.core.InstrumentedFragment; import com.android.settings.core.instrumentation.MetricsFeatureProvider; @@ -234,6 +237,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O mSavedQueryController.loadSavedQueries(); mSearchFeatureProvider.hideFeedbackButton(); } else { + mSearchAdapter.initializeSearch(mQuery); restartLoaders(); } @@ -270,15 +274,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O return; } - final int resultCount = mSearchAdapter.displaySearchResults(mQuery); - - if (resultCount == 0) { - mNoResultsView.setVisibility(View.VISIBLE); - } else { - mNoResultsView.setVisibility(View.GONE); - mResultsRecyclerView.scrollToPosition(0); - } - mSearchFeatureProvider.showFeedbackButton(this, getView()); + mSearchAdapter.notifyResultsLoaded(); } @Override @@ -304,30 +300,24 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O requery(); } - public void onSearchResultClicked(SearchViewHolder result, String settingName, + public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result, Pair... logTaggedData) { - final List> taggedData = new ArrayList<>(); - if (logTaggedData != null) { - taggedData.addAll(Arrays.asList(logTaggedData)); - } - taggedData.add(Pair.create( - MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_COUNT, - mSearchAdapter.getItemCount())); - taggedData.add(Pair.create( - MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_RANK, - result.getAdapterPosition())); - taggedData.add(Pair.create( - MetricsEvent.FIELD_SETTINGS_SERACH_QUERY_LENGTH, - TextUtils.isEmpty(mQuery) ? 0 : mQuery.length())); + logSearchResultClicked(resultViewHolder, result, logTaggedData); - mMetricsFeatureProvider.action(getContext(), - MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_RESULT, - settingName, - taggedData.toArray(new Pair[0])); mSavedQueryController.saveQuery(mQuery); mResultClickCount++; } + public void onSearchResultsDisplayed(int resultCount) { + if (resultCount == 0) { + mNoResultsView.setVisibility(View.VISIBLE); + } else { + mNoResultsView.setVisibility(View.GONE); + mResultsRecyclerView.scrollToPosition(0); + } + mSearchFeatureProvider.showFeedbackButton(this, getView()); + } + public void onSavedQueryClicked(CharSequence query) { final String queryString = query.toString(); mMetricsFeatureProvider.action(getContext(), @@ -378,4 +368,38 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O mResultsRecyclerView.requestFocus(); } } -} \ No newline at end of file + + private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result, + Pair... logTaggedData) { + final Intent intent = result.payload.getIntent(); + if (intent == null) { + Log.w(TAG, "Skipped logging click on search result because of null intent, which can " + + "happen on saved query results."); + return; + } + final ComponentName cn = intent.getComponent(); + String resultName = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT); + if (TextUtils.isEmpty(resultName) && cn != null) { + resultName = cn.flattenToString(); + } + final List> taggedData = new ArrayList<>(); + if (logTaggedData != null) { + taggedData.addAll(Arrays.asList(logTaggedData)); + } + taggedData.add(Pair.create( + MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_COUNT, + mSearchAdapter.getItemCount())); + taggedData.add(Pair.create( + MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_RANK, + resultViewHolder.getAdapterPosition())); + taggedData.add(Pair.create( + MetricsEvent.FIELD_SETTINGS_SERACH_QUERY_LENGTH, + TextUtils.isEmpty(mQuery) ? 0 : mQuery.length())); + + mMetricsFeatureProvider.action(getContext(), + MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_RESULT, + resultName, + taggedData.toArray(new Pair[0])); + mSearchFeatureProvider.searchResultClicked(getContext(), mQuery, result); + } +} diff --git a/src/com/android/settings/search/SearchResultsAdapter.java b/src/com/android/settings/search/SearchResultsAdapter.java index 31e07933a7f..199930f39e7 100644 --- a/src/com/android/settings/search/SearchResultsAdapter.java +++ b/src/com/android/settings/search/SearchResultsAdapter.java @@ -18,36 +18,79 @@ 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; -public class SearchResultsAdapter extends RecyclerView.Adapter { +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 int MSG_RANKING_TIMED_OUT = 1; + + // TODO(b/38197948): Tune this timeout based on latency of static and async rankings. Also, we + // should add a gservices flag to control this. + private static final long RANKING_TIMEOUT_MS = 300; private final SearchFragment mFragment; - - private List mSearchResults; + 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 {} + private static final int DISABLED = 0; + private static final int PENDING_RESULTS = 1; + private static final int SUCCEEDED = 2; + private static final int FAILED = 3; + private static final int TIMED_OUT = 4; + private @AsyncRankingState int mAsyncRankingState; public SearchResultsAdapter(SearchFragment fragment, SearchFeatureProvider searchFeatureProvider) { mFragment = fragment; + mContext = fragment.getContext().getApplicationContext(); mSearchResults = new ArrayList<>(); mResultsMap = new ArrayMap<>(); + mSearchRankingScores = new ArrayList<>(); + mStaticallyRankedSearchResults = new ArrayList<>(); mSearchFeatureProvider = searchFeatureProvider; setHasStableIds(true); @@ -93,7 +136,37 @@ 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. @@ -120,71 +193,24 @@ public class SearchResultsAdapter extends RecyclerView.Adapter } /** - * Merge the results from each of the loaders into one list for the adapter. - * Prioritizes results from the local database over installed apps. - * - * @param query user query corresponding to these results - * @return Number of matched results + * Notifies the adapter that all the unsorted results are loaded and now the ladapter can + * proceed with ranking the results. */ - public int displaySearchResults(String query) { - List databaseResults = null; - List installedAppResults = null; - final String dbLoaderKey = DatabaseResultLoader.class.getName(); - final String appLoaderKey = InstalledAppResultLoader.class.getName(); - int dbSize = 0; - int appSize = 0; - if (mResultsMap.containsKey(dbLoaderKey)) { - databaseResults = new ArrayList<>(mResultsMap.get(dbLoaderKey)); - dbSize = databaseResults.size(); - Collections.sort(databaseResults); + @MainThread + public void notifyResultsLoaded() { + mSearchResultsLoaded = true; + // static ranking is skipped only if asyc ranking is already succeeded. + if (mAsyncRankingState != SUCCEEDED) { + doStaticRanking(); } - if (mResultsMap.containsKey(appLoaderKey)) { - installedAppResults = new ArrayList<>(mResultsMap.get(appLoaderKey)); - appSize = installedAppResults.size(); - Collections.sort(installedAppResults); + if (canUpdateSearchResults()) { + updateSearchResults(); } - final List newResults = new ArrayList<>(dbSize + appSize); - - int dbIndex = 0; - int appIndex = 0; - int rank = SearchResult.TOP_RANK; - - while (rank <= SearchResult.BOTTOM_RANK) { - while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) { - newResults.add(databaseResults.get(dbIndex++)); - } - while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) { - newResults.add(installedAppResults.get(appIndex++)); - } - rank++; - } - - while (dbIndex < dbSize) { - newResults.add(databaseResults.get(dbIndex++)); - } - while (appIndex < appSize) { - newResults.add(installedAppResults.get(appIndex++)); - } - - final boolean isSmartSearchRankingEnabled = mSearchFeatureProvider - .isSmartSearchRankingEnabled(mFragment.getContext().getApplicationContext()); - - if (isSmartSearchRankingEnabled) { - // TODO: run this in parallel to loading the results if takes too long - mSearchFeatureProvider.rankSearchResults(query, newResults); - } - - final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( - new SearchResultDiffCallback(mSearchResults, newResults), - isSmartSearchRankingEnabled); - mSearchResults = newResults; - diffResult.dispatchUpdatesTo(this); - - return mSearchResults.size(); } public void clearResults() { mSearchResults.clear(); + mStaticallyRankedSearchResults.clear(); mResultsMap.clear(); notifyDataSetChanged(); } @@ -193,4 +219,178 @@ public class SearchResultsAdapter extends RecyclerView.Adapter 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(); + handler.sendMessageDelayed( + handler.obtainMessage(MSG_RANKING_TIMED_OUT), RANKING_TIMEOUT_MS); + mSearchFeatureProvider.querySearchResults(mContext, query, this); + } else { + mAsyncRankingState = DISABLED; + } + } + + /** + * 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); + int dbSize = databaseResults.size(); + int appSize = installedAppResults.size(); + + int dbIndex = 0; + int appIndex = 0; + int rank = SearchResult.TOP_RANK; + + 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++)); + } + rank++; + } + + while (dbIndex < dbSize) { + mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); + } + while (appIndex < appSize) { + mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); + } + } + + 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); + int dbSize = databaseResults.size(); + int appSize = installedAppResults.size(); + + final List asyncRankingResults = new ArrayList<>(dbSize + appSize); + List databaseResultsSortedByScores = new ArrayList<>(databaseResults); + Collections.sort(databaseResultsSortedByScores, 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; + } + } + }); + asyncRankingResults.addAll(databaseResultsSortedByScores); + // App results are not ranked by async ranking and appended at the end of the list. + asyncRankingResults.addAll(installedAppResults); + 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; + } + + private void postSearchResults(List newSearchResults, boolean detectMoves) { + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( + new SearchResultDiffCallback(mSearchResults, newSearchResults), detectMoves); + mSearchResults.clear(); + mSearchResults.addAll(newSearchResults); + diffResult.dispatchUpdatesTo(this); + mFragment.onSearchResultsDisplayed(mSearchResults.size()); + mSearchResultsUpdated = true; + } } diff --git a/src/com/android/settings/search/ranking/SearchResultsRankerCallback.java b/src/com/android/settings/search/ranking/SearchResultsRankerCallback.java new file mode 100644 index 00000000000..c254a40144c --- /dev/null +++ b/src/com/android/settings/search/ranking/SearchResultsRankerCallback.java @@ -0,0 +1,38 @@ +/* + * 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.ranking; + +import android.util.Pair; + +import java.util.List; + +public interface SearchResultsRankerCallback { + + /** + * Called when ranker provides the ranking scores. + * @param searchRankingScores Ordered List of Pairs of String and Float corresponding to + * stableIds and ranking scores. The list must be descendingly + * ordered based on scores. + */ + public void onRankingScoresAvailable(List> searchRankingScores); + + /** + * Called when for any reason ranker fails, which notifies the client to proceed + * without ranking results. + */ + public void onRankingFailed(); +} diff --git a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java index ba1408f7759..efeeffcfed5 100644 --- a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java +++ b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java @@ -97,7 +97,7 @@ public class IntentSearchViewHolderTest { assertThat(mHolder.summaryView.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mHolder.breadcrumbView.getVisibility()).isEqualTo(View.GONE); - verify(mFragment).onSearchResultClicked(eq(mHolder), anyString()); + verify(mFragment).onSearchResultClicked(eq(mHolder), any(SearchResult.class)); verify(mFragment).startActivity(any(Intent.class)); } diff --git a/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search/SearchFragmentTest.java index 44851b1e0c7..6adc895d570 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 android.app.LoaderManager; +import android.content.Intent; import android.content.Context; import android.content.Loader; import android.os.Bundle; @@ -26,6 +27,7 @@ import android.view.View; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; +import com.android.settings.SettingsActivity; import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; import com.android.settings.testutils.DatabaseTestUtils; @@ -36,7 +38,9 @@ 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; @@ -75,6 +79,11 @@ public class SearchFragmentTest { private SavedQueryLoader mSavedQueryLoader; @Mock private SavedQueryController mSavedQueryController; + @Mock + private SearchResultsAdapter mSearchResultsAdapter; + @Captor + private ArgumentCaptor mQueryCaptor = ArgumentCaptor.forClass(String.class); + private FakeFeatureFactory mFeatureFactory; @Before @@ -148,7 +157,7 @@ public class SearchFragmentTest { } @Test - public void queryTextChange_shouldTriggerLoader() { + public void queryTextChange_shouldTriggerLoaderAndInitializeSearch() { when(mFeatureFactory.searchFeatureProvider .getDatabaseSearchLoader(any(Context.class), anyString())) .thenReturn(mDatabaseResultLoader); @@ -167,6 +176,7 @@ public class SearchFragmentTest { when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class))) .thenReturn(true); + ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter); fragment.onQueryTextChange(testQuery); activityController.get().onBackPressed(); @@ -181,10 +191,12 @@ public class SearchFragmentTest { .getDatabaseSearchLoader(any(Context.class), anyString()); verify(mFeatureFactory.searchFeatureProvider) .getInstalledAppSearchLoader(any(Context.class), anyString()); + verify(mSearchResultsAdapter).initializeSearch(mQueryCaptor.capture()); + assertThat(mQueryCaptor.getValue()).isEqualTo(testQuery); } @Test - public void queryTextChangeToEmpty_shouldLoadSavedQuery() { + public void queryTextChangeToEmpty_shouldLoadSavedQueryAndNotInitializeSearch() { when(mFeatureFactory.searchFeatureProvider .getDatabaseSearchLoader(any(Context.class), anyString())) .thenReturn(mDatabaseResultLoader); @@ -201,6 +213,7 @@ public class SearchFragmentTest { when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class))) .thenReturn(true); ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController); + ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter); fragment.mQuery = "123"; fragment.onQueryTextChange(""); @@ -210,6 +223,7 @@ public class SearchFragmentTest { verify(mFeatureFactory.searchFeatureProvider, never()) .getInstalledAppSearchLoader(any(Context.class), anyString()); verify(mSavedQueryController).loadSavedQueries(); + verify(mSearchResultsAdapter, never()).initializeSearch(anyString()); } @Test @@ -383,12 +397,21 @@ public class SearchFragmentTest { SearchFragment fragment = new SearchFragment(); ReflectionHelpers.setField(fragment, "mMetricsFeatureProvider", mFeatureFactory.metricsFeatureProvider); + ReflectionHelpers.setField(fragment, "mSearchFeatureProvider", + mFeatureFactory.searchFeatureProvider); ReflectionHelpers.setField(fragment, "mSearchAdapter", mock(SearchResultsAdapter.class)); fragment.mSavedQueryController = mock(SavedQueryController.class); // Should log result name, result count, clicked rank, etc. - final SearchViewHolder result = mock(SearchViewHolder.class); - fragment.onSearchResultClicked(result, "test_setting"); + final SearchViewHolder resultViewHolder = mock(SearchViewHolder.class); + ResultPayload payLoad = new ResultPayload( + (new Intent()).putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, "test_setting")); + SearchResult searchResult = new SearchResult.Builder() + .setStableId(payLoad.hashCode()) + .setPayload(payLoad) + .setTitle("setting_title") + .build(); + fragment.onSearchResultClicked(resultViewHolder, searchResult); verify(mFeatureFactory.metricsFeatureProvider).action( nullable(Context.class), @@ -397,6 +420,9 @@ public class SearchFragmentTest { argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_COUNT)), argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_RANK)), argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_QUERY_LENGTH))); + + verify(mFeatureFactory.searchFeatureProvider).searchResultClicked(nullable(Context.class), + nullable(String.class), eq(searchResult)); } private ArgumentMatcher> pairMatches(int tag) { diff --git a/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java index 829034863f3..c6a54511883 100644 --- a/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchResultsAdapterTest.java @@ -21,6 +21,7 @@ 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; @@ -28,24 +29,31 @@ import com.android.settings.R; import com.android.settings.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; import com.android.settings.search.SearchResult.Builder; +import com.android.settings.search.ranking.SearchResultsRankerCallback; +import static org.junit.Assert.assertNull; 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 static org.mockito.Matchers.any; import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -63,6 +71,9 @@ 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; @@ -73,10 +84,10 @@ public class SearchResultsAdapterTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = Robolectric.buildActivity(Activity.class).get(); - mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider); mLoaderClassName = DatabaseResultLoader.class.getName(); when(mFragment.getContext()).thenReturn(mMockContext); when(mMockContext.getApplicationContext()).thenReturn(mContext); + mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider); } @Test @@ -88,8 +99,9 @@ public class SearchResultsAdapterTest { @Test public void testSingleSourceMerge_exactCopyReturned() { Set intentResults = getIntentSampleResults(); + mAdapter.initializeSearch(""); mAdapter.addSearchResults(intentResults, mLoaderClassName); - mAdapter.displaySearchResults(""); + mAdapter.notifyResultsLoaded(); List updatedResults = mAdapter.getSearchResults(); assertThat(updatedResults).containsAllIn(intentResults); @@ -113,11 +125,12 @@ 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()); - int count = mAdapter.displaySearchResults(""); + mAdapter.notifyResultsLoaded(); List results = mAdapter.getSearchResults(); assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha @@ -126,25 +139,28 @@ public class SearchResultsAdapterTest { 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 - assertThat(count).isEqualTo(6); + verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture()); + assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6); } @Test public void testEndToEndSearch_addResults_resultsAddedInOrder() { - List appResults = getDummyAppResults(); + 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.displaySearchResults(""); + 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()); - int count = mAdapter.displaySearchResults(""); + mAdapter.notifyResultsLoaded(); List results = mAdapter.getSearchResults(); assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha @@ -153,46 +169,333 @@ public class SearchResultsAdapterTest { 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 - assertThat(count).isEqualTo(6); - } - - @Test - public void testDisplayResults_ShouldNotRunSmartRankingIfDisabled() { - when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())) - .thenReturn(false); - mAdapter.displaySearchResults(""); - verify(mSearchFeatureProvider, never()).rankSearchResults(anyString(), anyList()); - } - - @Test - public void testDisplayResults_ShouldRunSmartRankingIfEnabled() { - when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())) - .thenReturn(true); - mAdapter.displaySearchResults(""); - verify(mSearchFeatureProvider, times(1)).rankSearchResults(anyString(), anyList()); + 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 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.displaySearchResults(""); + 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()); - int count = mAdapter.displaySearchResults(""); + mAdapter.notifyResultsLoaded(); List results = mAdapter.getSearchResults(); assertThat(results.get(0).title).isEqualTo(TITLES[0]); assertThat(results.get(1).title).isEqualTo(TITLES[3]); - assertThat(count).isEqualTo(2); + 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); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + } + + @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); + } + + @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)); + } + + @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)); + } + + @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 + } + } } private List getDummyDbResults() { @@ -218,8 +521,8 @@ public class SearchResultsAdapterTest { return results; } - private List getDummyAppResults() { - List results = new ArrayList<>(); + private List getDummyAppResults() { + List results = new ArrayList<>(); ResultPayload payload = new ResultPayload(new Intent()); AppSearchResult.Builder builder = new AppSearchResult.Builder(); builder.setPayload(payload) @@ -265,4 +568,16 @@ public class SearchResultsAdapterTest { sampleResults.add(builder.build()); return sampleResults; } -} \ No newline at end of file + + 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; + } +}