Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2e30bca8 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

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

parents feba9f5d 733bbf7c
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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);
        });
+2 −12
Original line number Diff line number Diff line
@@ -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());
        });
    }
}
+14 −4
Original line number Diff line number Diff line
@@ -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) {
    }

    /**
+52 −28
Original line number Diff line number Diff line
@@ -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);
    }
}
+247 −47
Original line number Diff line number Diff line
@@ -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);
@@ -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.
     *
@@ -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