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

Commit fb772248 authored by Matthew Fritze's avatar Matthew Fritze
Browse files

Move search querying into a single API

Settings now collects search results from a single
loader which fetches from an aggregator. This is to
facilitate the separation of search functionalitiy,
where "query" becomes a single synchronous call.
In this case, the aggregator will move to the
unbundled app and would be called on the
other end of the Query call. i.e. the new search
result loader will just call query, and unbundled
search will handle everything else.

An important implication is that the results will
be returned in a ranked order. Thus the ranking and
merging logic has been moved out of the RecyclerView
adapter (which is a good clean-up, anyway).

The SearchResultAggregator starts a Future for each
of the data sources:
- Static Results
- Installed Apps
- Input Devices
- Accessibility Services

We allow up to 500ms to collect the static results,
and then an additional 150ms for each subsequent
loader. In my quick tests, the static results take
about 20-30ms to load. The longest loader is installed
apps which takes roughly 50-60ms seconds (note that
this will be improved with dynamic result caching).

To handle the ranking in DatabaseResultLoader,
we start a Future to collect the dynamic ranking before
we start the SQL queries. When the SQL is done, we
wait the same timeout as before. Then we merge the
results, as before.

For now we have not changed how the Dynamic results
are collected, but eventually they will be a cache
of dynamic results.

Bug: 33577327
Bug: 67360547
Test: robotests
Change-Id: I91fb03f9fd059672a970f48bea21c8d655007fa3
parent 10d0518f
Loading
Loading
Loading
Loading
+89 −82
Original line number Diff line number Diff line
@@ -30,24 +30,36 @@ import android.os.UserHandle;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.util.IconDrawableFactory;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;

import com.android.settings.R;
import com.android.settings.accessibility.AccessibilitySettings;
import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.utils.AsyncLoader;

import java.util.HashSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
public class AccessibilityServiceResultLoader extends
        FutureTask<List<? extends SearchResult>> {

    private static final String TAG = "A11yResultFutureTask";

    public AccessibilityServiceResultLoader(Context context, String query,
            SiteMapManager manager) {
        super(new AccessibilityServiceResultCallable(context, query, manager));
    }

    static class AccessibilityServiceResultCallable implements
            Callable<List<? extends SearchResult>> {

        private static final int NAME_NO_MATCH = -1;

        private final Context mContext;

        private List<String> mBreadcrumb;
        private SiteMapManager mSiteMapManager;
        @VisibleForTesting
@@ -56,12 +68,10 @@ public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends
        private final PackageManager mPackageManager;
        private final int mUserId;


    public AccessibilityServiceResultLoader(Context context, String query,
        public AccessibilityServiceResultCallable(Context context, String query,
                SiteMapManager mapManager) {
        super(context);
        mContext = context;
            mUserId = UserHandle.myUserId();
            mContext = context;
            mSiteMapManager = mapManager;
            mPackageManager = context.getPackageManager();
            mAccessibilityManager =
@@ -70,13 +80,13 @@ public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends
        }

        @Override
    public Set<? extends SearchResult> loadInBackground() {
        final Set<SearchResult> results = new HashSet<>();
        final Context context = getContext();
        public List<? extends SearchResult> call() throws Exception {
            long startTime = System.currentTimeMillis();
            final List<SearchResult> results = new ArrayList<>();
            final List<AccessibilityServiceInfo> services = mAccessibilityManager
                    .getInstalledAccessibilityServiceList();
            final IconDrawableFactory iconFactory = IconDrawableFactory.newInstance(mContext);
        final String screenTitle = context.getString(R.string.accessibility_settings);
            final String screenTitle = mContext.getString(R.string.accessibility_settings);
            for (AccessibilityServiceInfo service : services) {
                if (service == null) {
                    continue;
@@ -93,7 +103,7 @@ public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends
                }
                final Drawable icon;
                if (resolveInfo.getIconResource() == 0) {
                icon = ContextCompat.getDrawable(context, R.mipmap.ic_accessibility_generic);
                    icon = ContextCompat.getDrawable(mContext, R.mipmap.ic_accessibility_generic);
                } else {
                    icon = iconFactory.getBadgedIcon(
                            resolveInfo.serviceInfo,
@@ -102,7 +112,7 @@ public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends
                }
                final String componentName = new ComponentName(serviceInfo.packageName,
                        serviceInfo.name).flattenToString();
            final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context,
                final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(mContext,
                        AccessibilitySettings.class.getName(), componentName, screenTitle);

                results.add(new SearchResult.Builder()
@@ -114,21 +124,18 @@ public class AccessibilityServiceResultLoader extends AsyncLoader<Set<? extends
                        .setStableId(Objects.hash(screenTitle, componentName))
                        .build());
            }
            Collections.sort(results);
            Log.i(TAG, "A11y search loading took:" + (System.currentTimeMillis() - startTime));
            return results;
        }

        private List<String> getBreadCrumb() {
            if (mBreadcrumb == null || mBreadcrumb.isEmpty()) {
            final Context context = getContext();
                mBreadcrumb = mSiteMapManager.buildBreadCrumb(
                    context, AccessibilitySettings.class.getName(),
                    context.getString(R.string.accessibility_settings));
                        mContext, AccessibilitySettings.class.getName(),
                        mContext.getString(R.string.accessibility_settings));
            }
            return mBreadcrumb;
        }

    @Override
    protected void onDiscardResult(Set<? extends SearchResult> result) {

    }
}
+19 −10
Original line number Diff line number Diff line
@@ -36,16 +36,6 @@ import java.util.Map;
import java.util.Set;

import static com.android.settings.search.DatabaseResultLoader.BASE_RANKS;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_CLASS_NAME;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ICON;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_PAYLOAD_TYPE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_SCREEN_TITLE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_SUMMARY_ON;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_TITLE;
import static com.android.settings.search.SearchResult.TOP_RANK;

/**
@@ -62,6 +52,25 @@ public class CursorToSearchResultConverter {

    private static final String TAG = "CursorConverter";

    /**
     * These indices are used to match the columns of the this loader's SELECT statement.
     * These are not necessarily the same order nor similar coverage as the schema defined in
     * IndexDatabaseHelper
     */
    public static final int COLUMN_INDEX_ID = 0;
    public static final int COLUMN_INDEX_TITLE = 1;
    public static final int COLUMN_INDEX_SUMMARY_ON = 2;
    public static final int COLUMN_INDEX_SUMMARY_OFF = 3;
    public static final int COLUMN_INDEX_CLASS_NAME = 4;
    public static final int COLUMN_INDEX_SCREEN_TITLE = 5;
    public static final int COLUMN_INDEX_ICON = 6;
    public static final int COLUMN_INDEX_INTENT_ACTION = 7;
    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
    public static final int COLUMN_INDEX_KEY = 10;
    public static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
    public static final int COLUMN_INDEX_PAYLOAD = 12;

    private final Context mContext;

    private final int LONG_TITLE_LENGTH = 20;
+5 −4
Original line number Diff line number Diff line
@@ -17,11 +17,13 @@

package com.android.settings.search;

import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID;
import static com.android.settings.search.DatabaseResultLoader

import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_ID;
import static com.android.settings.search.CursorToSearchResultConverter
        .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE;
import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_KEY;
import static com.android.settings.search.CursorToSearchResultConverter.COLUMN_INDEX_KEY;
import static com.android.settings.search.DatabaseResultLoader.SELECT_COLUMNS;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.CLASS_NAME;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_ENTRIES;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_KEYWORDS;
@@ -31,7 +33,6 @@ import static com.android.settings.search.IndexDatabaseHelper.IndexColumns
        .DATA_SUMMARY_ON_NORMALIZED;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DATA_TITLE_NORMALIZED;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.DOCID;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ENABLED;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.ICON;
import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.INTENT_ACTION;
+247 −178
Original line number Diff line number Diff line
@@ -24,35 +24,31 @@ import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.util.Pair;

import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.utils.AsyncLoader;
import com.android.settings.overlay.FeatureFactory;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * AsyncTask to retrieve Settings, First party app and any intent based results.
 * AsyncTask to retrieve Settings, first party app and any intent based results.
 */
public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult>> {
    private static final String LOG = "DatabaseResultLoader";

    /* These indices are used to match the columns of the this loader's SELECT statement.
     These are not necessarily the same order nor similar coverage as the schema defined in
     IndexDatabaseHelper */
    public static final int COLUMN_INDEX_ID = 0;
    public static final int COLUMN_INDEX_TITLE = 1;
    public static final int COLUMN_INDEX_SUMMARY_ON = 2;
    public static final int COLUMN_INDEX_SUMMARY_OFF = 3;
    public static final int COLUMN_INDEX_CLASS_NAME = 4;
    public static final int COLUMN_INDEX_SCREEN_TITLE = 5;
    public static final int COLUMN_INDEX_ICON = 6;
    public static final int COLUMN_INDEX_INTENT_ACTION = 7;
    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE = 8;
    public static final int COLUMN_INDEX_INTENT_ACTION_TARGET_CLASS = 9;
    public static final int COLUMN_INDEX_KEY = 10;
    public static final int COLUMN_INDEX_PAYLOAD_TYPE = 11;
    public static final int COLUMN_INDEX_PAYLOAD = 12;
public class DatabaseResultLoader extends FutureTask<List<? extends SearchResult>> {

    private static final String TAG = "DatabaseResultLoader";

    public static final String[] SELECT_COLUMNS = {
            IndexColumns.DOCID,
@@ -82,62 +78,92 @@ public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult
            IndexColumns.DATA_SUMMARY_OFF_NORMALIZED,
    };

    public static final String[] MATCH_COLUMNS_TERTIARY = {
            IndexColumns.DATA_KEYWORDS,
            IndexColumns.DATA_ENTRIES
    };

    /**
     * Base ranks defines the best possible rank based on what the query matches.
     * If the query matches the prefix of the first word in the title, the best rank it can be is 1
     * If the query matches the prefix of the other words in the title, the best rank it can be is 3
     * If the query matches the prefix of the first word in the title, the best rank it can be
     * is 1
     * If the query matches the prefix of the other words in the title, the best rank it can be
     * is 3
     * If the query only matches the summary, the best rank it can be is 7
     * If the query only matches keywords or entries, the best rank it can be is 9
     */
    public static final int[] BASE_RANKS = {1, 3, 7, 9};

    public DatabaseResultLoader(Context context, String query, SiteMapManager manager) {
        super(new StaticSearchResultCallable(context, query, manager));
    }

    static class StaticSearchResultCallable implements
            Callable<List<? extends SearchResult>> {

        public final String[] MATCH_COLUMNS_TERTIARY = {
                IndexColumns.DATA_KEYWORDS,
                IndexColumns.DATA_ENTRIES
        };

        @VisibleForTesting
        final String mQueryText;
        private final Context mContext;
        private final CursorToSearchResultConverter mConverter;
        private final SiteMapManager mSiteMapManager;
        private final SearchFeatureProvider mFeatureProvider;

    public DatabaseResultLoader(Context context, String queryText, SiteMapManager mapManager) {
        super(context);
        mSiteMapManager = mapManager;
        public StaticSearchResultCallable(Context context, String queryText,
                SiteMapManager mapManager) {
            mContext = context;
            mSiteMapManager = mapManager;
            mQueryText = queryText;
            mConverter = new CursorToSearchResultConverter(context);
            mFeatureProvider = FeatureFactory.getFactory(context).getSearchFeatureProvider();
        }

        @Override
    protected void onDiscardResult(Set<? extends SearchResult> result) {
        // TODO Search
    }

    @Override
    public Set<? extends SearchResult> loadInBackground() {
        public List<? extends SearchResult> call() {
            if (mQueryText == null || mQueryText.isEmpty()) {
            return null;
                return new ArrayList<>();
            }

        final Set<SearchResult> results = new HashSet<>();
            // TODO (b/68656233) Consolidate timing metrics
            long startTime = System.currentTimeMillis();
            // Start a Future to get search result scores.
            FutureTask<List<Pair<String, Float>>> rankerTask = mFeatureProvider.getRankerTask(
                    mContext, mQueryText);

        results.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
        results.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
        results.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
        results.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));
        return results;
            if (rankerTask != null) {
                ExecutorService executorService = mFeatureProvider.getExecutorService();
                executorService.execute(rankerTask);
            }

    @Override
    protected boolean onCancelLoad() {
        // TODO
        return super.onCancelLoad();
            final Set<SearchResult> resultSet = new HashSet<>();

            resultSet.addAll(firstWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[0]));
            resultSet.addAll(secondaryWordQuery(MATCH_COLUMNS_PRIMARY, BASE_RANKS[1]));
            resultSet.addAll(anyWordQuery(MATCH_COLUMNS_SECONDARY, BASE_RANKS[2]));
            resultSet.addAll(anyWordQuery(MATCH_COLUMNS_TERTIARY, BASE_RANKS[3]));

            // Try to retrieve the scores in time. Otherwise use static ranking.
            if (rankerTask != null) {
                try {
                    final long timeoutMs = mFeatureProvider.smartSearchRankingTimeoutMs(mContext);
                    List<Pair<String, Float>> searchRankScores = rankerTask.get(timeoutMs,
                            TimeUnit.MILLISECONDS);
                    return getDynamicRankedResults(resultSet, searchRankScores);
                } catch (TimeoutException | InterruptedException | ExecutionException e) {
                    Log.d(TAG, "Error waiting for result scores: " + e);
                }
            }

            List<SearchResult> resultList = new ArrayList<>(resultSet);
            Collections.sort(resultList);
            Log.i(TAG, "Static search loading took:" + (System.currentTimeMillis() - startTime));
            return resultList;
        }

        // TODO (b/33577327) Retrieve all search results with a single query.

        /**
     * Creates and executes the query which matches prefixes of the first word of the given columns.
         * Creates and executes the query which matches prefixes of the first word of the given
         * columns.
         *
         * @param matchColumns The columns to match on
         * @param baseRank     The highest rank achievable by these results
@@ -168,7 +194,8 @@ public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult
        }

        /**
     * Creates and executes the query which matches prefixes of the any word of the given columns.
         * Creates and executes the query which matches prefixes of the any word of the given
         * columns.
         *
         * @param matchColumns The columns to match on
         * @param baseRank     The highest rank achievable by these results
@@ -192,7 +219,8 @@ public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult
        private Set<SearchResult> query(String whereClause, String[] selection, int baseRank) {
            final SQLiteDatabase database =
                    IndexDatabaseHelper.getInstance(mContext).getReadableDatabase();
        try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause,
            try (Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS,
                    whereClause,
                    selection, null, null, null)) {
                return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank);
            }
@@ -257,7 +285,8 @@ public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult
        /**
         * Fills out the selection array to match the query as the prefix of a word.
         *
     * @param size is twice the number of columns to be matched. The first match is for the prefix
         * @param size is twice the number of columns to be matched. The first match is for the
         *             prefix
         *             of the first word in the column. The second match is for any subsequent word
         *             prefix match.
         */
@@ -272,4 +301,44 @@ public class DatabaseResultLoader extends AsyncLoader<Set<? extends SearchResult
            }
            return selection;
        }

        private List<SearchResult> getDynamicRankedResults(Set<SearchResult> unsortedSet,
                List<Pair<String, Float>> searchRankScores) {
            TreeSet<SearchResult> dbResultsSortedByScores = new TreeSet<>(
                    (o1, o2) -> {
                        float score1 = getRankingScoreByStableId(searchRankScores, o1.stableId);
                        float score2 = getRankingScoreByStableId(searchRankScores, o2.stableId);
                        if (score1 > score2) {
                            return -1;
                        } else if (score1 == score2) {
                            return 0;
                        } else {
                            return 1;
                        }
                    });
            dbResultsSortedByScores.addAll(unsortedSet);

            return new ArrayList<>(dbResultsSortedByScores);
        }

        /**
         * 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(List<Pair<String, Float>> searchRankScores, int stableId) {
            for (Pair<String, Float> rankingScore : searchRankScores) {
                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;
        }
    }
}
 No newline at end of file
+133 −122

File changed.

Preview size limit exceeded, changes collapsed.

Loading