Loading src/com/android/settings/search/InlineSwitchViewHolder.java +2 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); }); Loading src/com/android/settings/search/IntentSearchViewHolder.java +2 −12 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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()); }); } } src/com/android/settings/search/SearchFeatureProvider.java +14 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { } /** Loading src/com/android/settings/search/SearchFragment.java +52 −28 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -234,6 +237,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O mSavedQueryController.loadSavedQueries(); mSearchFeatureProvider.hideFeedbackButton(); } else { mSearchAdapter.initializeSearch(mQuery); restartLoaders(); } Loading Loading @@ -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 Loading @@ -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(), Loading Loading @@ -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); } } src/com/android/settings/search/SearchResultsAdapter.java +247 −47 Original line number Diff line number Diff line Loading @@ -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"; private final SearchFragment mFragment; @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; private List<SearchResult> mSearchResults; // 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); Loading Loading @@ -93,6 +136,36 @@ 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. * Loading @@ -119,78 +192,205 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> 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. * * @param query user query corresponding to these results * @return Number of matched 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); } if (mResultsMap.containsKey(appLoaderKey)) { installedAppResults = new ArrayList<>(mResultsMap.get(appLoaderKey)); appSize = installedAppResults.size(); Collections.sort(installedAppResults); } final List<SearchResult> newResults = new ArrayList<>(dbSize + appSize); 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)) { newResults.add(databaseResults.get(dbIndex++)); mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); } while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) { newResults.add(installedAppResults.get(appIndex++)); mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); } rank++; } while (dbIndex < dbSize) { newResults.add(databaseResults.get(dbIndex++)); mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); } while (appIndex < appSize) { newResults.add(installedAppResults.get(appIndex++)); mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); } } final boolean isSmartSearchRankingEnabled = mSearchFeatureProvider .isSmartSearchRankingEnabled(mFragment.getContext().getApplicationContext()); 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; } } if (isSmartSearchRankingEnabled) { // TODO: run this in parallel to loading the results if takes too long mSearchFeatureProvider.rankSearchResults(query, newResults); 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; } final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( new SearchResultDiffCallback(mSearchResults, newResults), isSmartSearchRankingEnabled); mSearchResults = newResults; diffResult.dispatchUpdatesTo(this); @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(); return mSearchResults.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; } public void clearResults() { mSearchResults.clear(); mResultsMap.clear(); notifyDataSetChanged(); @VisibleForTesting Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) { return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<SearchResult>(); } @VisibleForTesting public List<SearchResult> getSearchResults() { return mSearchResults; 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; } } Loading
src/com/android/settings/search/InlineSwitchViewHolder.java +2 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); }); Loading
src/com/android/settings/search/IntentSearchViewHolder.java +2 −12 Original line number Diff line number Diff line Loading @@ -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. Loading @@ -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()); }); } }
src/com/android/settings/search/SearchFeatureProvider.java +14 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { } /** Loading
src/com/android/settings/search/SearchFragment.java +52 −28 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -234,6 +237,7 @@ public class SearchFragment extends InstrumentedFragment implements SearchView.O mSavedQueryController.loadSavedQueries(); mSearchFeatureProvider.hideFeedbackButton(); } else { mSearchAdapter.initializeSearch(mQuery); restartLoaders(); } Loading Loading @@ -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 Loading @@ -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(), Loading Loading @@ -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); } }
src/com/android/settings/search/SearchResultsAdapter.java +247 −47 Original line number Diff line number Diff line Loading @@ -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"; private final SearchFragment mFragment; @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; private List<SearchResult> mSearchResults; // 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); Loading Loading @@ -93,6 +136,36 @@ 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. * Loading @@ -119,78 +192,205 @@ public class SearchResultsAdapter extends RecyclerView.Adapter<SearchViewHolder> 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. * * @param query user query corresponding to these results * @return Number of matched 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); } if (mResultsMap.containsKey(appLoaderKey)) { installedAppResults = new ArrayList<>(mResultsMap.get(appLoaderKey)); appSize = installedAppResults.size(); Collections.sort(installedAppResults); } final List<SearchResult> newResults = new ArrayList<>(dbSize + appSize); 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)) { newResults.add(databaseResults.get(dbIndex++)); mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); } while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) { newResults.add(installedAppResults.get(appIndex++)); mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); } rank++; } while (dbIndex < dbSize) { newResults.add(databaseResults.get(dbIndex++)); mStaticallyRankedSearchResults.add(databaseResults.get(dbIndex++)); } while (appIndex < appSize) { newResults.add(installedAppResults.get(appIndex++)); mStaticallyRankedSearchResults.add(installedAppResults.get(appIndex++)); } } final boolean isSmartSearchRankingEnabled = mSearchFeatureProvider .isSmartSearchRankingEnabled(mFragment.getContext().getApplicationContext()); 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; } } if (isSmartSearchRankingEnabled) { // TODO: run this in parallel to loading the results if takes too long mSearchFeatureProvider.rankSearchResults(query, newResults); 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; } final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff( new SearchResultDiffCallback(mSearchResults, newResults), isSmartSearchRankingEnabled); mSearchResults = newResults; diffResult.dispatchUpdatesTo(this); @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(); return mSearchResults.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; } public void clearResults() { mSearchResults.clear(); mResultsMap.clear(); notifyDataSetChanged(); @VisibleForTesting Set<? extends SearchResult> getUnsortedLoadedResults(String loaderKey) { return mResultsMap.containsKey(loaderKey) ? mResultsMap.get(loaderKey) : new HashSet<SearchResult>(); } @VisibleForTesting public List<SearchResult> getSearchResults() { return mSearchResults; 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; } }