Updating the search ranking API and some improvements:
- Ranking API is modified to run the ranking asynchronous to the main thread. Therefore, it can now run in parallel to loading the results from DB which decreases the overall latency. - Ranking API now supports reporting failure from the ranker implementation side. - Settings that are not ranked by the ranker algorithm are now ranked at the end of the list. This is added for dynamic settings (e.g., apps). - Failure handling mechanism is added for cases that ranker catches an exception or it takes a long time to respond. Bug: 37312700 Fixes: 36866337 Fixes: 36867476 Fixes: 36866736 Fixes: 36866838 Test: RunSettingsRoboTests Change-Id: I3a2a97e3a07a8d4afbb090061d92172a27588ee7
This commit is contained in:
@@ -46,7 +46,7 @@ public class InlineSwitchViewHolder extends SearchViewHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBind(SearchFragment fragment, SearchResult result) {
|
public void onBind(SearchFragment fragment, final SearchResult result) {
|
||||||
super.onBind(fragment, result);
|
super.onBind(fragment, result);
|
||||||
if (mContext == null) {
|
if (mContext == null) {
|
||||||
return;
|
return;
|
||||||
@@ -57,7 +57,7 @@ public class InlineSwitchViewHolder extends SearchViewHolder {
|
|||||||
final Pair<Integer, Object> value = Pair.create(
|
final Pair<Integer, Object> value = Pair.create(
|
||||||
MetricsEvent.FIELD_SETTINGS_SEARCH_INLINE_RESULT_VALUE, isChecked
|
MetricsEvent.FIELD_SETTINGS_SEARCH_INLINE_RESULT_VALUE, isChecked
|
||||||
? 1L : 0L);
|
? 1L : 0L);
|
||||||
fragment.onSearchResultClicked(this, payload.mSettingKey, value);
|
fragment.onSearchResultClicked(this, result, value);
|
||||||
int newValue = isChecked ? InlineSwitchPayload.TRUE : InlineSwitchPayload.FALSE;
|
int newValue = isChecked ? InlineSwitchPayload.TRUE : InlineSwitchPayload.FALSE;
|
||||||
payload.setValue(mContext, newValue);
|
payload.setValue(mContext, newValue);
|
||||||
});
|
});
|
||||||
|
@@ -16,13 +16,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.android.settings.search;
|
package com.android.settings.search;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||||
import com.android.settings.SettingsActivity;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewHolder for intent based search results.
|
* ViewHolder for intent based search results.
|
||||||
@@ -44,14 +40,8 @@ public class IntentSearchViewHolder extends SearchViewHolder {
|
|||||||
super.onBind(fragment, result);
|
super.onBind(fragment, result);
|
||||||
|
|
||||||
itemView.setOnClickListener(v -> {
|
itemView.setOnClickListener(v -> {
|
||||||
final Intent intent = result.payload.getIntent();
|
fragment.onSearchResultClicked(this, result);
|
||||||
final ComponentName cn = intent.getComponent();
|
fragment.startActivity(result.payload.getIntent());
|
||||||
String resultName = intent.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT);
|
|
||||||
if (TextUtils.isEmpty(resultName) && cn != null) {
|
|
||||||
resultName = cn.flattenToString();
|
|
||||||
}
|
|
||||||
fragment.onSearchResultClicked(this, resultName);
|
|
||||||
fragment.startActivity(intent);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,7 @@ import android.view.Menu;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.android.settings.dashboard.SiteMapManager;
|
import com.android.settings.dashboard.SiteMapManager;
|
||||||
|
import com.android.settings.search.ranking.SearchResultsRankerCallback;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.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 query input user query
|
||||||
* @param searchResults list of search results to be ranked
|
* @param searchResultsRankerCallback {@link SearchResultsRankerCallback}
|
||||||
*/
|
*/
|
||||||
default void rankSearchResults(String query, List<SearchResult> 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.
|
* Notify that a search result is clicked.
|
||||||
*
|
*
|
||||||
|
* @param context application context
|
||||||
* @param query input user query
|
* @param query input user query
|
||||||
* @param searchResult clicked result
|
* @param searchResult clicked result
|
||||||
*/
|
*/
|
||||||
default void searchResultClicked(String query, SearchResult searchResult) {
|
default void searchResultClicked(Context context, String query, SearchResult searchResult) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -19,7 +19,9 @@ package com.android.settings.search;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.LoaderManager;
|
import android.app.LoaderManager;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.Loader;
|
import android.content.Loader;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
@@ -37,6 +39,7 @@ import android.widget.SearchView;
|
|||||||
|
|
||||||
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.SettingsActivity;
|
||||||
import com.android.settings.Utils;
|
import com.android.settings.Utils;
|
||||||
import com.android.settings.core.InstrumentedFragment;
|
import com.android.settings.core.InstrumentedFragment;
|
||||||
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
|
import com.android.settings.core.instrumentation.MetricsFeatureProvider;
|
||||||
@@ -234,6 +237,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
|
|||||||
mSavedQueryController.loadSavedQueries();
|
mSavedQueryController.loadSavedQueries();
|
||||||
mSearchFeatureProvider.hideFeedbackButton();
|
mSearchFeatureProvider.hideFeedbackButton();
|
||||||
} else {
|
} else {
|
||||||
|
mSearchAdapter.initializeSearch(mQuery);
|
||||||
restartLoaders();
|
restartLoaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,15 +274,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int resultCount = mSearchAdapter.displaySearchResults(mQuery);
|
mSearchAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
if (resultCount == 0) {
|
|
||||||
mNoResultsView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
mNoResultsView.setVisibility(View.GONE);
|
|
||||||
mResultsRecyclerView.scrollToPosition(0);
|
|
||||||
}
|
|
||||||
mSearchFeatureProvider.showFeedbackButton(this, getView());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -304,30 +300,24 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
|
|||||||
requery();
|
requery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSearchResultClicked(SearchViewHolder result, String settingName,
|
public void onSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
|
||||||
Pair<Integer, Object>... logTaggedData) {
|
Pair<Integer, Object>... logTaggedData) {
|
||||||
final List<Pair<Integer, Object>> taggedData = new ArrayList<>();
|
logSearchResultClicked(resultViewHolder, result, logTaggedData);
|
||||||
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()));
|
|
||||||
|
|
||||||
mMetricsFeatureProvider.action(getContext(),
|
|
||||||
MetricsEvent.ACTION_CLICK_SETTINGS_SEARCH_RESULT,
|
|
||||||
settingName,
|
|
||||||
taggedData.toArray(new Pair[0]));
|
|
||||||
mSavedQueryController.saveQuery(mQuery);
|
mSavedQueryController.saveQuery(mQuery);
|
||||||
mResultClickCount++;
|
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) {
|
public void onSavedQueryClicked(CharSequence query) {
|
||||||
final String queryString = query.toString();
|
final String queryString = query.toString();
|
||||||
mMetricsFeatureProvider.action(getContext(),
|
mMetricsFeatureProvider.action(getContext(),
|
||||||
@@ -378,4 +368,38 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O
|
|||||||
mResultsRecyclerView.requestFocus();
|
mResultsRecyclerView.requestFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void logSearchResultClicked(SearchViewHolder resultViewHolder, SearchResult result,
|
||||||
|
Pair<Integer, Object>... 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<Pair<Integer, Object>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -18,36 +18,79 @@
|
|||||||
package com.android.settings.search;
|
package com.android.settings.search;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.MainThread;
|
import android.support.annotation.MainThread;
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.support.v7.util.DiffUtil;
|
import android.support.v7.util.DiffUtil;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.ArrayMap;
|
import android.util.ArrayMap;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.search.ranking.SearchResultsRankerCallback;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> {
|
public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
|
||||||
|
implements SearchResultsRankerCallback {
|
||||||
|
private static final String TAG = "SearchResultsAdapter";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String DB_RESULTS_LOADER_KEY = DatabaseResultLoader.class.getName();
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String APP_RESULTS_LOADER_KEY = InstalledAppResultLoader.class.getName();
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final 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 final SearchFragment mFragment;
|
||||||
|
private final Context mContext;
|
||||||
private List<SearchResult> mSearchResults;
|
private final List<SearchResult> mSearchResults;
|
||||||
|
private final List<SearchResult> mStaticallyRankedSearchResults;
|
||||||
private Map<String, Set<? extends SearchResult>> mResultsMap;
|
private Map<String, Set<? extends SearchResult>> mResultsMap;
|
||||||
private final SearchFeatureProvider mSearchFeatureProvider;
|
private final SearchFeatureProvider mSearchFeatureProvider;
|
||||||
|
private List<Pair<String, Float>> mSearchRankingScores;
|
||||||
|
private Handler mHandler;
|
||||||
|
private boolean mSearchResultsLoaded;
|
||||||
|
private boolean mSearchResultsUpdated;
|
||||||
|
|
||||||
|
@IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT})
|
||||||
|
@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,
|
public SearchResultsAdapter(SearchFragment fragment,
|
||||||
SearchFeatureProvider searchFeatureProvider) {
|
SearchFeatureProvider searchFeatureProvider) {
|
||||||
mFragment = fragment;
|
mFragment = fragment;
|
||||||
|
mContext = fragment.getContext().getApplicationContext();
|
||||||
mSearchResults = new ArrayList<>();
|
mSearchResults = new ArrayList<>();
|
||||||
mResultsMap = new ArrayMap<>();
|
mResultsMap = new ArrayMap<>();
|
||||||
|
mSearchRankingScores = new ArrayList<>();
|
||||||
|
mStaticallyRankedSearchResults = new ArrayList<>();
|
||||||
mSearchFeatureProvider = searchFeatureProvider;
|
mSearchFeatureProvider = searchFeatureProvider;
|
||||||
|
|
||||||
setHasStableIds(true);
|
setHasStableIds(true);
|
||||||
@@ -93,7 +136,37 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
|
|||||||
return mSearchResults.size();
|
return mSearchResults.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@MainThread
|
||||||
|
@Override
|
||||||
|
public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores) {
|
||||||
|
// Received the scores, stop the timeout timer.
|
||||||
|
getHandler().removeMessages(MSG_RANKING_TIMED_OUT);
|
||||||
|
if (mAsyncRankingState == PENDING_RESULTS) {
|
||||||
|
mAsyncRankingState = SUCCEEDED;
|
||||||
|
mSearchRankingScores.clear();
|
||||||
|
mSearchRankingScores.addAll(searchRankingScores);
|
||||||
|
if (canUpdateSearchResults()) {
|
||||||
|
updateSearchResults();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Ranking scores became available in invalid state: " + mAsyncRankingState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
@Override
|
||||||
|
public void onRankingFailed() {
|
||||||
|
if (mAsyncRankingState == PENDING_RESULTS) {
|
||||||
|
mAsyncRankingState = FAILED;
|
||||||
|
if (canUpdateSearchResults()) {
|
||||||
|
updateSearchResults();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Ranking scores failed in invalid states: " + mAsyncRankingState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Store the results from each of the loaders to be merged when all loaders are finished.
|
* Store the results from each of the loaders to be merged when all loaders are finished.
|
||||||
*
|
*
|
||||||
* @param results the results from the loader.
|
* @param results the results from the loader.
|
||||||
@@ -120,71 +193,24 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge the results from each of the loaders into one list for the adapter.
|
* Notifies the adapter that all the unsorted results are loaded and now the ladapter can
|
||||||
* Prioritizes results from the local database over installed apps.
|
* proceed with ranking the results.
|
||||||
*
|
|
||||||
* @param query user query corresponding to these results
|
|
||||||
* @return Number of matched results
|
|
||||||
*/
|
*/
|
||||||
public int displaySearchResults(String query) {
|
@MainThread
|
||||||
List<? extends SearchResult> databaseResults = null;
|
public void notifyResultsLoaded() {
|
||||||
List<? extends SearchResult> installedAppResults = null;
|
mSearchResultsLoaded = true;
|
||||||
final String dbLoaderKey = DatabaseResultLoader.class.getName();
|
// static ranking is skipped only if asyc ranking is already succeeded.
|
||||||
final String appLoaderKey = InstalledAppResultLoader.class.getName();
|
if (mAsyncRankingState != SUCCEEDED) {
|
||||||
int dbSize = 0;
|
doStaticRanking();
|
||||||
int appSize = 0;
|
|
||||||
if (mResultsMap.containsKey(dbLoaderKey)) {
|
|
||||||
databaseResults = new ArrayList<>(mResultsMap.get(dbLoaderKey));
|
|
||||||
dbSize = databaseResults.size();
|
|
||||||
Collections.sort(databaseResults);
|
|
||||||
}
|
}
|
||||||
if (mResultsMap.containsKey(appLoaderKey)) {
|
if (canUpdateSearchResults()) {
|
||||||
installedAppResults = new ArrayList<>(mResultsMap.get(appLoaderKey));
|
updateSearchResults();
|
||||||
appSize = installedAppResults.size();
|
|
||||||
Collections.sort(installedAppResults);
|
|
||||||
}
|
}
|
||||||
final List<SearchResult> 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() {
|
public void clearResults() {
|
||||||
mSearchResults.clear();
|
mSearchResults.clear();
|
||||||
|
mStaticallyRankedSearchResults.clear();
|
||||||
mResultsMap.clear();
|
mResultsMap.clear();
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -193,4 +219,178 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
|
|||||||
public List<SearchResult> getSearchResults() {
|
public List<SearchResult> getSearchResults() {
|
||||||
return mSearchResults;
|
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<? extends SearchResult> databaseResults =
|
||||||
|
getSortedLoadedResults(DB_RESULTS_LOADER_KEY);
|
||||||
|
List<? extends SearchResult> 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<SearchResult> doAsyncRanking() {
|
||||||
|
Set<? extends SearchResult> databaseResults =
|
||||||
|
getUnsortedLoadedResults(DB_RESULTS_LOADER_KEY);
|
||||||
|
List<? extends SearchResult> installedAppResults =
|
||||||
|
getSortedLoadedResults(APP_RESULTS_LOADER_KEY);
|
||||||
|
int dbSize = databaseResults.size();
|
||||||
|
int appSize = installedAppResults.size();
|
||||||
|
|
||||||
|
final List<SearchResult> asyncRankingResults = new ArrayList<>(dbSize + appSize);
|
||||||
|
List<SearchResult> databaseResultsSortedByScores = new ArrayList<>(databaseResults);
|
||||||
|
Collections.sort(databaseResultsSortedByScores, new Comparator<SearchResult>() {
|
||||||
|
@Override
|
||||||
|
public int compare(SearchResult o1, SearchResult o2) {
|
||||||
|
float score1 = getRankingScoreByStableId(o1.stableId);
|
||||||
|
float score2 = getRankingScoreByStableId(o2.stableId);
|
||||||
|
if (score1 > score2) {
|
||||||
|
return -1;
|
||||||
|
} else if (score1 == score2) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) {
|
||||||
|
return mResultsMap.containsKey(loaderKey) ?
|
||||||
|
mResultsMap.get(loaderKey) : new HashSet<SearchResult>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
List<? extends SearchResult> getSortedLoadedResults(String loaderKey) {
|
||||||
|
List<? extends SearchResult> sortedLoadedResults =
|
||||||
|
new ArrayList<>(getUnsortedLoadedResults(loaderKey));
|
||||||
|
Collections.sort(sortedLoadedResults);
|
||||||
|
return sortedLoadedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up ranking score for stableId
|
||||||
|
* @param stableId String of stableId
|
||||||
|
* @return the ranking score corresponding to the given stableId. If there is no score
|
||||||
|
* available for this stableId, -Float.MAX_VALUE is returned.
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
Float getRankingScoreByStableId(int stableId) {
|
||||||
|
for (Pair<String, Float> rankingScore : mSearchRankingScores) {
|
||||||
|
if (Integer.toString(stableId).compareTo(rankingScore.first) == 0) {
|
||||||
|
return rankingScore.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If stableId not found in the list, we assign the minimum score so it will appear at
|
||||||
|
// the end of the list.
|
||||||
|
Log.w(TAG, "stableId " + stableId + " was not in the ranking scores.");
|
||||||
|
return -Float.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
Handler getHandler() {
|
||||||
|
if (mHandler == null) {
|
||||||
|
mHandler = new Handler(Looper.getMainLooper()) {
|
||||||
|
@Override
|
||||||
|
public void handleMessage(Message msg) {
|
||||||
|
if (msg.what == MSG_RANKING_TIMED_OUT) {
|
||||||
|
mSearchFeatureProvider.cancelPendingSearchQuery(mContext);
|
||||||
|
if (mAsyncRankingState == PENDING_RESULTS) {
|
||||||
|
mAsyncRankingState = TIMED_OUT;
|
||||||
|
if (canUpdateSearchResults()) {
|
||||||
|
updateSearchResults();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Ranking scores timed out in invalid state: " +
|
||||||
|
mAsyncRankingState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return mHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postSearchResults(List<SearchResult> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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<Pair<String, Float>> searchRankingScores);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when for any reason ranker fails, which notifies the client to proceed
|
||||||
|
* without ranking results.
|
||||||
|
*/
|
||||||
|
public void onRankingFailed();
|
||||||
|
}
|
@@ -97,7 +97,7 @@ public class IntentSearchViewHolderTest {
|
|||||||
assertThat(mHolder.summaryView.getVisibility()).isEqualTo(View.VISIBLE);
|
assertThat(mHolder.summaryView.getVisibility()).isEqualTo(View.VISIBLE);
|
||||||
assertThat(mHolder.breadcrumbView.getVisibility()).isEqualTo(View.GONE);
|
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));
|
verify(mFragment).startActivity(any(Intent.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
package com.android.settings.search;
|
package com.android.settings.search;
|
||||||
|
|
||||||
import android.app.LoaderManager;
|
import android.app.LoaderManager;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Loader;
|
import android.content.Loader;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -26,6 +27,7 @@ import android.view.View;
|
|||||||
|
|
||||||
import com.android.internal.logging.nano.MetricsProto;
|
import com.android.internal.logging.nano.MetricsProto;
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
|
import com.android.settings.SettingsActivity;
|
||||||
import com.android.settings.SettingsRobolectricTestRunner;
|
import com.android.settings.SettingsRobolectricTestRunner;
|
||||||
import com.android.settings.TestConfig;
|
import com.android.settings.TestConfig;
|
||||||
import com.android.settings.testutils.DatabaseTestUtils;
|
import com.android.settings.testutils.DatabaseTestUtils;
|
||||||
@@ -36,7 +38,9 @@ import org.junit.Before;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Answers;
|
import org.mockito.Answers;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.ArgumentMatcher;
|
import org.mockito.ArgumentMatcher;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.Robolectric;
|
import org.robolectric.Robolectric;
|
||||||
@@ -75,6 +79,11 @@ public class SearchFragmentTest {
|
|||||||
private SavedQueryLoader mSavedQueryLoader;
|
private SavedQueryLoader mSavedQueryLoader;
|
||||||
@Mock
|
@Mock
|
||||||
private SavedQueryController mSavedQueryController;
|
private SavedQueryController mSavedQueryController;
|
||||||
|
@Mock
|
||||||
|
private SearchResultsAdapter mSearchResultsAdapter;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<String> mQueryCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
|
||||||
private FakeFeatureFactory mFeatureFactory;
|
private FakeFeatureFactory mFeatureFactory;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -148,7 +157,7 @@ public class SearchFragmentTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void queryTextChange_shouldTriggerLoader() {
|
public void queryTextChange_shouldTriggerLoaderAndInitializeSearch() {
|
||||||
when(mFeatureFactory.searchFeatureProvider
|
when(mFeatureFactory.searchFeatureProvider
|
||||||
.getDatabaseSearchLoader(any(Context.class), anyString()))
|
.getDatabaseSearchLoader(any(Context.class), anyString()))
|
||||||
.thenReturn(mDatabaseResultLoader);
|
.thenReturn(mDatabaseResultLoader);
|
||||||
@@ -167,6 +176,7 @@ public class SearchFragmentTest {
|
|||||||
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
|
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
|
|
||||||
|
ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter);
|
||||||
fragment.onQueryTextChange(testQuery);
|
fragment.onQueryTextChange(testQuery);
|
||||||
activityController.get().onBackPressed();
|
activityController.get().onBackPressed();
|
||||||
|
|
||||||
@@ -181,10 +191,12 @@ public class SearchFragmentTest {
|
|||||||
.getDatabaseSearchLoader(any(Context.class), anyString());
|
.getDatabaseSearchLoader(any(Context.class), anyString());
|
||||||
verify(mFeatureFactory.searchFeatureProvider)
|
verify(mFeatureFactory.searchFeatureProvider)
|
||||||
.getInstalledAppSearchLoader(any(Context.class), anyString());
|
.getInstalledAppSearchLoader(any(Context.class), anyString());
|
||||||
|
verify(mSearchResultsAdapter).initializeSearch(mQueryCaptor.capture());
|
||||||
|
assertThat(mQueryCaptor.getValue()).isEqualTo(testQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void queryTextChangeToEmpty_shouldLoadSavedQuery() {
|
public void queryTextChangeToEmpty_shouldLoadSavedQueryAndNotInitializeSearch() {
|
||||||
when(mFeatureFactory.searchFeatureProvider
|
when(mFeatureFactory.searchFeatureProvider
|
||||||
.getDatabaseSearchLoader(any(Context.class), anyString()))
|
.getDatabaseSearchLoader(any(Context.class), anyString()))
|
||||||
.thenReturn(mDatabaseResultLoader);
|
.thenReturn(mDatabaseResultLoader);
|
||||||
@@ -201,6 +213,7 @@ public class SearchFragmentTest {
|
|||||||
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
|
when(mFeatureFactory.searchFeatureProvider.isIndexingComplete(any(Context.class)))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController);
|
ReflectionHelpers.setField(fragment, "mSavedQueryController", mSavedQueryController);
|
||||||
|
ReflectionHelpers.setField(fragment, "mSearchAdapter", mSearchResultsAdapter);
|
||||||
fragment.mQuery = "123";
|
fragment.mQuery = "123";
|
||||||
|
|
||||||
fragment.onQueryTextChange("");
|
fragment.onQueryTextChange("");
|
||||||
@@ -210,6 +223,7 @@ public class SearchFragmentTest {
|
|||||||
verify(mFeatureFactory.searchFeatureProvider, never())
|
verify(mFeatureFactory.searchFeatureProvider, never())
|
||||||
.getInstalledAppSearchLoader(any(Context.class), anyString());
|
.getInstalledAppSearchLoader(any(Context.class), anyString());
|
||||||
verify(mSavedQueryController).loadSavedQueries();
|
verify(mSavedQueryController).loadSavedQueries();
|
||||||
|
verify(mSearchResultsAdapter, never()).initializeSearch(anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -383,12 +397,21 @@ public class SearchFragmentTest {
|
|||||||
SearchFragment fragment = new SearchFragment();
|
SearchFragment fragment = new SearchFragment();
|
||||||
ReflectionHelpers.setField(fragment, "mMetricsFeatureProvider",
|
ReflectionHelpers.setField(fragment, "mMetricsFeatureProvider",
|
||||||
mFeatureFactory.metricsFeatureProvider);
|
mFeatureFactory.metricsFeatureProvider);
|
||||||
|
ReflectionHelpers.setField(fragment, "mSearchFeatureProvider",
|
||||||
|
mFeatureFactory.searchFeatureProvider);
|
||||||
ReflectionHelpers.setField(fragment, "mSearchAdapter", mock(SearchResultsAdapter.class));
|
ReflectionHelpers.setField(fragment, "mSearchAdapter", mock(SearchResultsAdapter.class));
|
||||||
fragment.mSavedQueryController = mock(SavedQueryController.class);
|
fragment.mSavedQueryController = mock(SavedQueryController.class);
|
||||||
|
|
||||||
// Should log result name, result count, clicked rank, etc.
|
// Should log result name, result count, clicked rank, etc.
|
||||||
final SearchViewHolder result = mock(SearchViewHolder.class);
|
final SearchViewHolder resultViewHolder = mock(SearchViewHolder.class);
|
||||||
fragment.onSearchResultClicked(result, "test_setting");
|
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(
|
verify(mFeatureFactory.metricsFeatureProvider).action(
|
||||||
nullable(Context.class),
|
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_COUNT)),
|
||||||
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_RANK)),
|
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_RESULT_RANK)),
|
||||||
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_QUERY_LENGTH)));
|
argThat(pairMatches(MetricsProto.MetricsEvent.FIELD_SETTINGS_SERACH_QUERY_LENGTH)));
|
||||||
|
|
||||||
|
verify(mFeatureFactory.searchFeatureProvider).searchResultClicked(nullable(Context.class),
|
||||||
|
nullable(String.class), eq(searchResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ArgumentMatcher<Pair<Integer, Object>> pairMatches(int tag) {
|
private ArgumentMatcher<Pair<Integer, Object>> pairMatches(int tag) {
|
||||||
|
@@ -21,6 +21,7 @@ import android.app.Activity;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.util.Pair;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
@@ -28,24 +29,31 @@ import com.android.settings.R;
|
|||||||
import com.android.settings.SettingsRobolectricTestRunner;
|
import com.android.settings.SettingsRobolectricTestRunner;
|
||||||
import com.android.settings.TestConfig;
|
import com.android.settings.TestConfig;
|
||||||
import com.android.settings.search.SearchResult.Builder;
|
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.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.robolectric.Robolectric;
|
import org.robolectric.Robolectric;
|
||||||
import org.robolectric.annotation.Config;
|
import org.robolectric.annotation.Config;
|
||||||
|
import org.robolectric.shadows.ShadowLooper;
|
||||||
|
|
||||||
import static org.mockito.Matchers.any;
|
import static org.mockito.Matchers.any;
|
||||||
import static org.mockito.Matchers.anyList;
|
import static org.mockito.Matchers.anyList;
|
||||||
import static org.mockito.Matchers.anyString;
|
import static org.mockito.Matchers.anyString;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -63,6 +71,9 @@ public class SearchResultsAdapterTest {
|
|||||||
private SearchFeatureProvider mSearchFeatureProvider;
|
private SearchFeatureProvider mSearchFeatureProvider;
|
||||||
@Mock
|
@Mock
|
||||||
private Context mMockContext;
|
private Context mMockContext;
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Integer> mSearchResultsCountCaptor =
|
||||||
|
ArgumentCaptor.forClass(Integer.class);
|
||||||
private SearchResultsAdapter mAdapter;
|
private SearchResultsAdapter mAdapter;
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private String mLoaderClassName;
|
private String mLoaderClassName;
|
||||||
@@ -73,10 +84,10 @@ public class SearchResultsAdapterTest {
|
|||||||
public void setUp() {
|
public void setUp() {
|
||||||
MockitoAnnotations.initMocks(this);
|
MockitoAnnotations.initMocks(this);
|
||||||
mContext = Robolectric.buildActivity(Activity.class).get();
|
mContext = Robolectric.buildActivity(Activity.class).get();
|
||||||
mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider);
|
|
||||||
mLoaderClassName = DatabaseResultLoader.class.getName();
|
mLoaderClassName = DatabaseResultLoader.class.getName();
|
||||||
when(mFragment.getContext()).thenReturn(mMockContext);
|
when(mFragment.getContext()).thenReturn(mMockContext);
|
||||||
when(mMockContext.getApplicationContext()).thenReturn(mContext);
|
when(mMockContext.getApplicationContext()).thenReturn(mContext);
|
||||||
|
mAdapter = new SearchResultsAdapter(mFragment, mSearchFeatureProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -88,8 +99,9 @@ public class SearchResultsAdapterTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testSingleSourceMerge_exactCopyReturned() {
|
public void testSingleSourceMerge_exactCopyReturned() {
|
||||||
Set<SearchResult> intentResults = getIntentSampleResults();
|
Set<SearchResult> intentResults = getIntentSampleResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
mAdapter.addSearchResults(intentResults, mLoaderClassName);
|
mAdapter.addSearchResults(intentResults, mLoaderClassName);
|
||||||
mAdapter.displaySearchResults("");
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
List<SearchResult> updatedResults = mAdapter.getSearchResults();
|
List<SearchResult> updatedResults = mAdapter.getSearchResults();
|
||||||
assertThat(updatedResults).containsAllIn(intentResults);
|
assertThat(updatedResults).containsAllIn(intentResults);
|
||||||
@@ -113,11 +125,12 @@ public class SearchResultsAdapterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEndToEndSearch_properResultsMerged_correctOrder() {
|
public void testEndToEndSearch_properResultsMerged_correctOrder() {
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(getDummyAppResults()),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(getDummyAppResults()),
|
||||||
InstalledAppResultLoader.class.getName());
|
InstalledAppResultLoader.class.getName());
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(getDummyDbResults()),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(getDummyDbResults()),
|
||||||
DatabaseResultLoader.class.getName());
|
DatabaseResultLoader.class.getName());
|
||||||
int count = mAdapter.displaySearchResults("");
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
List<SearchResult> results = mAdapter.getSearchResults();
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
|
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(3).title).isEqualTo(TITLES[1]); // bravo
|
||||||
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
||||||
assertThat(count).isEqualTo(6);
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEndToEndSearch_addResults_resultsAddedInOrder() {
|
public void testEndToEndSearch_addResults_resultsAddedInOrder() {
|
||||||
List<AppSearchResult> appResults = getDummyAppResults();
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
List<SearchResult> dbResults = getDummyDbResults();
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
// Add two individual items
|
// Add two individual items
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(appResults.subList(0, 1)),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(appResults.subList(0, 1)),
|
||||||
InstalledAppResultLoader.class.getName());
|
InstalledAppResultLoader.class.getName());
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(dbResults.subList(0, 1)),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(dbResults.subList(0, 1)),
|
||||||
DatabaseResultLoader.class.getName());
|
DatabaseResultLoader.class.getName());
|
||||||
mAdapter.displaySearchResults("");
|
mAdapter.notifyResultsLoaded();
|
||||||
// Add super-set of items
|
// Add super-set of items
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
mAdapter.addSearchResults(
|
mAdapter.addSearchResults(
|
||||||
new HashSet<SearchResult>(appResults), InstalledAppResultLoader.class.getName());
|
new HashSet<SearchResult>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
mAdapter.addSearchResults(
|
mAdapter.addSearchResults(
|
||||||
new HashSet<SearchResult>(dbResults), DatabaseResultLoader.class.getName());
|
new HashSet<SearchResult>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
int count = mAdapter.displaySearchResults("");
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
List<SearchResult> results = mAdapter.getSearchResults();
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
|
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(3).title).isEqualTo(TITLES[1]); // bravo
|
||||||
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
||||||
assertThat(count).isEqualTo(6);
|
verify(mFragment, times(2)).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
}
|
assertThat(mSearchResultsCountCaptor.getAllValues().toArray())
|
||||||
|
.isEqualTo(new Integer[] {2, 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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testEndToEndSearch_removeResults_resultsAdded() {
|
public void testEndToEndSearch_removeResults_resultsAdded() {
|
||||||
List<AppSearchResult> appResults = getDummyAppResults();
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
List<SearchResult> dbResults = getDummyDbResults();
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
// Add list of items
|
// Add list of items
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(appResults),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(appResults),
|
||||||
InstalledAppResultLoader.class.getName());
|
InstalledAppResultLoader.class.getName());
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(dbResults),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(dbResults),
|
||||||
DatabaseResultLoader.class.getName());
|
DatabaseResultLoader.class.getName());
|
||||||
mAdapter.displaySearchResults("");
|
mAdapter.notifyResultsLoaded();
|
||||||
// Add subset of items
|
// Add subset of items
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
mAdapter.addSearchResults(new HashSet<SearchResult>(appResults.subList(0, 1)),
|
mAdapter.addSearchResults(new HashSet<SearchResult>(appResults.subList(0, 1)),
|
||||||
InstalledAppResultLoader.class.getName());
|
InstalledAppResultLoader.class.getName());
|
||||||
mAdapter.addSearchResults(new HashSet<>(dbResults.subList(0, 1)),
|
mAdapter.addSearchResults(new HashSet<>(dbResults.subList(0, 1)),
|
||||||
DatabaseResultLoader.class.getName());
|
DatabaseResultLoader.class.getName());
|
||||||
int count = mAdapter.displaySearchResults("");
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
List<SearchResult> results = mAdapter.getSearchResults();
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
assertThat(results.get(0).title).isEqualTo(TITLES[0]);
|
assertThat(results.get(0).title).isEqualTo(TITLES[0]);
|
||||||
assertThat(results.get(1).title).isEqualTo(TITLES[3]);
|
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<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
|
||||||
|
|
||||||
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEndToEndSearch_smartSearchRankingEnabledAndSucceededBeforeResultsLoaded() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEndToEndSearch_smartSearchRankingEnabledAndFailedAfterResultsLoaded() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
mAdapter.onRankingFailed();
|
||||||
|
|
||||||
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEndToEndSearch_smartSearchRankingEnabledAndFailedBeforeResultsLoaded() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.onRankingFailed();
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEndToEndSearch_smartSearchRankingEnabledAndTimedoutAfterResultsLoaded() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
|
waitUntilRankingTimesOut();
|
||||||
|
|
||||||
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEndToEndSearch_smartSearchRankingEnabledAndTimedoutBeforeResultsLoaded() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
|
||||||
|
waitUntilRankingTimesOut();
|
||||||
|
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
|
||||||
|
List<SearchResult> results = mAdapter.getSearchResults();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
verify(mFragment).onSearchResultsDisplayed(mSearchResultsCountCaptor.capture());
|
||||||
|
assertThat(mSearchResultsCountCaptor.getValue()).isEqualTo(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoSmartRanking_shouldRankAppResultsAfterDbResults() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
|
||||||
|
List<SearchResult> results = mAdapter.doAsyncRanking();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDoSmartRanking_shouldRankResultsWithMissingScoresAfterScoredResults() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(true);
|
||||||
|
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
List<Pair<String, Float>> rankingScores = getDummyRankingScores();
|
||||||
|
rankingScores.remove(1); // no ranking score for alpha
|
||||||
|
mAdapter.onRankingScoresAvailable(rankingScores);
|
||||||
|
List<SearchResult> results = mAdapter.doAsyncRanking();
|
||||||
|
assertThat(results.get(0).title).isEqualTo(TITLES[2]); // charlie
|
||||||
|
assertThat(results.get(1).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(results.get(2).title).isEqualTo(TITLES[0]); // alpha
|
||||||
|
assertThat(results.get(3).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(results.get(4).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(results.get(5).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetUnsortedLoadedResults () {
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
Set<CharSequence> expectedDbTitles = new HashSet<>(
|
||||||
|
Arrays.asList("alpha", "bravo", "charlie"));
|
||||||
|
Set<CharSequence> expectedAppTitles = new HashSet<>(
|
||||||
|
Arrays.asList("appAlpha", "appBravo", "appCharlie"));
|
||||||
|
Set<CharSequence> actualDbTitles = new HashSet<>();
|
||||||
|
Set<CharSequence> actualAppTitles = new HashSet<>();
|
||||||
|
for (SearchResult result : mAdapter.getUnsortedLoadedResults(SearchResultsAdapter
|
||||||
|
.DB_RESULTS_LOADER_KEY)) {
|
||||||
|
actualDbTitles.add(result.title);
|
||||||
|
}
|
||||||
|
for (SearchResult result : mAdapter.getUnsortedLoadedResults(SearchResultsAdapter
|
||||||
|
.APP_RESULTS_LOADER_KEY)) {
|
||||||
|
actualAppTitles.add(result.title);
|
||||||
|
}
|
||||||
|
assertThat(actualDbTitles).isEqualTo(expectedDbTitles);
|
||||||
|
assertThat(actualAppTitles).isEqualTo(expectedAppTitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetSortedLoadedResults() {
|
||||||
|
List<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(appResults), InstalledAppResultLoader.class.getName());
|
||||||
|
mAdapter.addSearchResults(
|
||||||
|
new HashSet<>(dbResults), DatabaseResultLoader.class.getName());
|
||||||
|
List<? extends SearchResult> actualDbResults =
|
||||||
|
mAdapter.getSortedLoadedResults(SearchResultsAdapter.DB_RESULTS_LOADER_KEY);
|
||||||
|
List<? extends SearchResult> actualAppResults =
|
||||||
|
mAdapter.getSortedLoadedResults(SearchResultsAdapter.APP_RESULTS_LOADER_KEY);
|
||||||
|
assertThat(actualDbResults.get(0).title).isEqualTo(TITLES[0]); // charlie
|
||||||
|
assertThat(actualDbResults.get(1).title).isEqualTo(TITLES[1]); // bravo
|
||||||
|
assertThat(actualDbResults.get(2).title).isEqualTo(TITLES[2]); // alpha
|
||||||
|
assertThat(actualAppResults.get(0).title).isEqualTo(TITLES[3]); // appAlpha
|
||||||
|
assertThat(actualAppResults.get(1).title).isEqualTo(TITLES[4]); // appBravo
|
||||||
|
assertThat(actualAppResults.get(2).title).isEqualTo(TITLES[5]); // appCharlie
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInitializeSearch_shouldNotRunSmartRankingIfDisabled() {
|
||||||
|
when(mSearchFeatureProvider.isSmartSearchRankingEnabled(any())).thenReturn(false);
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.notifyResultsLoaded();
|
||||||
|
verify(mSearchFeatureProvider, never()).querySearchResults(
|
||||||
|
any(Context.class), anyString(), any(SearchResultsRankerCallback.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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<SearchResult> appResults = getDummyAppResults();
|
||||||
|
List<SearchResult> dbResults = getDummyDbResults();
|
||||||
|
mAdapter.initializeSearch("");
|
||||||
|
mAdapter.onRankingScoresAvailable(getDummyRankingScores());
|
||||||
|
assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(0).stableId))
|
||||||
|
.isWithin(1e-10f).of(0.8f);
|
||||||
|
assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(1).stableId))
|
||||||
|
.isWithin(1e-10f).of(0.2f);
|
||||||
|
assertThat(mAdapter.getRankingScoreByStableId(dbResults.get(2).stableId))
|
||||||
|
.isWithin(1e-10f).of(0.9f);
|
||||||
|
assertThat(mAdapter.getRankingScoreByStableId(appResults.get(0).stableId))
|
||||||
|
.isEqualTo(-Float.MAX_VALUE);
|
||||||
|
assertThat(mAdapter.getRankingScoreByStableId(appResults.get(1).stableId))
|
||||||
|
.isEqualTo(-Float.MAX_VALUE);
|
||||||
|
assertThat(mAdapter.getRankingScoreByStableId(appResults.get(2).stableId))
|
||||||
|
.isEqualTo(-Float.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void waitUntilRankingTimesOut() {
|
||||||
|
while (mAdapter.getHandler().hasMessages(mAdapter.MSG_RANKING_TIMED_OUT)) {
|
||||||
|
try {
|
||||||
|
ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SearchResult> getDummyDbResults() {
|
private List<SearchResult> getDummyDbResults() {
|
||||||
@@ -218,8 +521,8 @@ public class SearchResultsAdapterTest {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AppSearchResult> getDummyAppResults() {
|
private List<SearchResult> getDummyAppResults() {
|
||||||
List<AppSearchResult> results = new ArrayList<>();
|
List<SearchResult> results = new ArrayList<>();
|
||||||
ResultPayload payload = new ResultPayload(new Intent());
|
ResultPayload payload = new ResultPayload(new Intent());
|
||||||
AppSearchResult.Builder builder = new AppSearchResult.Builder();
|
AppSearchResult.Builder builder = new AppSearchResult.Builder();
|
||||||
builder.setPayload(payload)
|
builder.setPayload(payload)
|
||||||
@@ -265,4 +568,16 @@ public class SearchResultsAdapterTest {
|
|||||||
sampleResults.add(builder.build());
|
sampleResults.add(builder.build());
|
||||||
return sampleResults;
|
return sampleResults;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private List<Pair<String, Float>> getDummyRankingScores() {
|
||||||
|
List<SearchResult> results = getDummyDbResults();
|
||||||
|
List<Pair<String, Float>> scores = new ArrayList<>();
|
||||||
|
scores.add(
|
||||||
|
new Pair<String, Float>(Long.toString(results.get(2).stableId), 0.9f)); // charlie
|
||||||
|
scores.add(
|
||||||
|
new Pair<String, Float>(Long.toString(results.get(0).stableId), 0.8f)); // alpha
|
||||||
|
scores.add(
|
||||||
|
new Pair<String, Float>(Long.toString(results.get(1).stableId), 0.2f)); // bravo
|
||||||
|
return scores;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user