- 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
397 lines
15 KiB
Java
397 lines
15 KiB
Java
/*
|
|
* Copyright (C) 2017 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package com.android.settings.search;
|
|
|
|
import android.content.Context;
|
|
import 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>
|
|
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 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);
|
|
}
|
|
|
|
@Override
|
|
public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
|
final Context context = parent.getContext();
|
|
final LayoutInflater inflater = LayoutInflater.from(context);
|
|
final View view;
|
|
switch (viewType) {
|
|
case ResultPayload.PayloadType.INTENT:
|
|
view = inflater.inflate(R.layout.search_intent_item, parent, false);
|
|
return new IntentSearchViewHolder(view);
|
|
case ResultPayload.PayloadType.INLINE_SWITCH:
|
|
view = inflater.inflate(R.layout.search_inline_switch_item, parent, false);
|
|
return new InlineSwitchViewHolder(view, context);
|
|
case ResultPayload.PayloadType.SAVED_QUERY:
|
|
view = inflater.inflate(R.layout.search_saved_query_item, parent, false);
|
|
return new SavedQueryViewHolder(view);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onBindViewHolder(SearchViewHolder holder, int position) {
|
|
holder.onBind(mFragment, mSearchResults.get(position));
|
|
}
|
|
|
|
@Override
|
|
public long getItemId(int position) {
|
|
return mSearchResults.get(position).stableId;
|
|
}
|
|
|
|
@Override
|
|
public int getItemViewType(int position) {
|
|
return mSearchResults.get(position).viewType;
|
|
}
|
|
|
|
@Override
|
|
public int getItemCount() {
|
|
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.
|
|
* @param loaderClassName class name of the loader.
|
|
*/
|
|
@MainThread
|
|
public void addSearchResults(Set<? extends SearchResult> results, String loaderClassName) {
|
|
if (results == null) {
|
|
return;
|
|
}
|
|
mResultsMap.put(loaderClassName, results);
|
|
}
|
|
|
|
/**
|
|
* Displays recent searched queries.
|
|
*
|
|
* @return The number of saved queries to display
|
|
*/
|
|
public int displaySavedQuery(List<? extends SearchResult> data) {
|
|
clearResults();
|
|
mSearchResults.addAll(data);
|
|
notifyDataSetChanged();
|
|
return mSearchResults.size();
|
|
}
|
|
|
|
/**
|
|
* Notifies the adapter that all the unsorted results are loaded and now the ladapter can
|
|
* proceed with ranking the results.
|
|
*/
|
|
@MainThread
|
|
public void notifyResultsLoaded() {
|
|
mSearchResultsLoaded = true;
|
|
// static ranking is skipped only if asyc ranking is already succeeded.
|
|
if (mAsyncRankingState != SUCCEEDED) {
|
|
doStaticRanking();
|
|
}
|
|
if (canUpdateSearchResults()) {
|
|
updateSearchResults();
|
|
}
|
|
}
|
|
|
|
public void clearResults() {
|
|
mSearchResults.clear();
|
|
mStaticallyRankedSearchResults.clear();
|
|
mResultsMap.clear();
|
|
notifyDataSetChanged();
|
|
}
|
|
|
|
@VisibleForTesting
|
|
public List<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;
|
|
}
|
|
}
|