Merge "Updating the search ranking API and some improvements:"

This commit is contained in:
TreeHugger Robot
2017-06-02 20:49:26 +00:00
committed by Android (Google) Code Review
9 changed files with 747 additions and 144 deletions

View File

@@ -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<Integer, Object> 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);
});

View File

@@ -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());
});
}
}

View File

@@ -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<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.
*
* @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) {
}
/**

View File

@@ -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<Integer, Object>... logTaggedData) {
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,
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();
}
}
}
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);
}
}

View File

@@ -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<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 List<SearchResult> mSearchResults;
private final Context mContext;
private final List<SearchResult> mSearchResults;
private final List<SearchResult> mStaticallyRankedSearchResults;
private Map<String, Set<? extends SearchResult>> mResultsMap;
private final SearchFeatureProvider mSearchFeatureProvider;
private List<Pair<String, Float>> mSearchRankingScores;
private Handler mHandler;
private boolean mSearchResultsLoaded;
private boolean mSearchResultsUpdated;
@IntDef({DISABLED, PENDING_RESULTS, SUCCEEDED, FAILED, TIMED_OUT})
@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<SearchViewHolder>
return mSearchResults.size();
}
/**
@MainThread
@Override
public void onRankingScoresAvailable(List<Pair<String, Float>> searchRankingScores) {
// Received the scores, stop the timeout timer.
getHandler().removeMessages(MSG_RANKING_TIMED_OUT);
if (mAsyncRankingState == PENDING_RESULTS) {
mAsyncRankingState = SUCCEEDED;
mSearchRankingScores.clear();
mSearchRankingScores.addAll(searchRankingScores);
if (canUpdateSearchResults()) {
updateSearchResults();
}
} else {
Log.w(TAG, "Ranking scores became available in invalid state: " + mAsyncRankingState);
}
}
@MainThread
@Override
public void onRankingFailed() {
if (mAsyncRankingState == PENDING_RESULTS) {
mAsyncRankingState = FAILED;
if (canUpdateSearchResults()) {
updateSearchResults();
}
} else {
Log.w(TAG, "Ranking scores failed in invalid states: " + mAsyncRankingState);
}
}
/**
* Store the results from each of the loaders to be merged when all loaders are finished.
*
* @param results the results from the loader.
@@ -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.
* 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<? extends SearchResult> databaseResults = null;
List<? extends SearchResult> 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<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() {
mSearchResults.clear();
mStaticallyRankedSearchResults.clear();
mResultsMap.clear();
notifyDataSetChanged();
}
@@ -193,4 +219,178 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder>
public List<SearchResult> 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<? 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;
}
}

View File

@@ -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();
}