Sync search result loaders

The loaders should be syncronized so the results can be
properly ranked without sudden insertions.

This means InstalledAppsLoader needs to finish faster,
which is accomplished by delaying icon loading to bind time
rather than as the apps are queried.

Bug: 34772522
Test: make RunSettingsRoboTests
Change-Id: I7f5244c574d37c6cfd8bbd0d3d40488f38211be3
This commit is contained in:
Matthew Fritze
2017-02-02 14:55:49 -08:00
parent 249077a0cd
commit 40ce0fab75
17 changed files with 473 additions and 103 deletions

View File

@@ -0,0 +1,45 @@
/*
* 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.search2;
import android.content.pm.ApplicationInfo;
public class AppSearchResult extends SearchResult {
/**
* Installed app's ApplicationInfo for delayed loading of icons
*/
public final ApplicationInfo info;
public AppSearchResult(Builder builder) {
super(builder);
info = builder.mInfo;
}
public static class Builder extends SearchResult.Builder {
protected ApplicationInfo mInfo;
public SearchResult.Builder setAppInfo(ApplicationInfo info) {
mInfo = info;
return this;
}
public AppSearchResult build() {
return new AppSearchResult(this);
}
}
}

View File

@@ -36,7 +36,7 @@ import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS
/**
* AsyncTask to retrieve Settings, First party app and any intent based results.
*/
public class DatabaseResultLoader extends AsyncLoader<List<SearchResult>> {
public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResult>> {
private static final String LOG = "DatabaseResultLoader";
/* These indices are used to match the columns of the this loader's SELECT statement.
@@ -98,26 +98,25 @@ public class DatabaseResultLoader extends AsyncLoader<List<SearchResult>> {
private static final int[] BASE_RANKS = {1, 4, 7};
private final String mQueryText;
private final SQLiteDatabase mDatabase;
private final Context mContext;
private final CursorToSearchResultConverter mConverter;
private final SiteMapManager mSiteMapManager;
public DatabaseResultLoader(Context context, String queryText) {
public DatabaseResultLoader(Context context, String queryText, SiteMapManager mapManager) {
super(context);
mSiteMapManager = FeatureFactory.getFactory(context)
.getSearchFeatureProvider().getSiteMapManager();
mDatabase = IndexDatabaseHelper.getInstance(context).getReadableDatabase();
mSiteMapManager = mapManager;
mContext = context;
mQueryText = cleanQuery(queryText);
mConverter = new CursorToSearchResultConverter(context, mQueryText);
}
@Override
protected void onDiscardResult(List<SearchResult> result) {
protected void onDiscardResult(List<? extends SearchResult> result) {
// TODO Search
}
@Override
public List<SearchResult> loadInBackground() {
public List<? extends SearchResult> loadInBackground() {
if (mQueryText == null || mQueryText.isEmpty()) {
return null;
}
@@ -144,7 +143,9 @@ public class DatabaseResultLoader extends AsyncLoader<List<SearchResult>> {
final String whereClause = buildWhereClause(matchColumns);
final String[] selection = buildQuerySelection(matchColumns.length * 2);
final Cursor resultCursor = mDatabase.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause,
final SQLiteDatabase database = IndexDatabaseHelper.getInstance(mContext)
.getReadableDatabase();
final Cursor resultCursor = database.query(TABLE_PREFS_INDEX, SELECT_COLUMNS, whereClause,
selection, null, null, null);
return mConverter.convertCursor(mSiteMapManager, resultCursor, baseRank);
}

View File

@@ -42,7 +42,7 @@ import java.util.List;
/**
* Search loader for installed apps.
*/
public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
public class InstalledAppResultLoader extends AsyncLoader<List<? extends SearchResult>> {
private static final int NAME_NO_MATCH = -1;
private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN)
@@ -56,18 +56,17 @@ public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper,
String query) {
String query, SiteMapManager mapManager) {
super(context);
mSiteMapManager = FeatureFactory.getFactory(context)
.getSearchFeatureProvider().getSiteMapManager();
mSiteMapManager = mapManager;
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mPackageManager = pmWrapper;
mQuery = query;
}
@Override
public List<SearchResult> loadInBackground() {
final List<SearchResult> results = new ArrayList<>();
public List<? extends SearchResult> loadInBackground() {
final List<AppSearchResult> results = new ArrayList<>();
final PackageManager pm = mPackageManager.getPackageManager();
for (UserInfo user : getUsersToCount()) {
@@ -90,10 +89,10 @@ public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", info.packageName, null));
final SearchResult.Builder builder = new SearchResult.Builder();
builder.addIcon(info.loadIcon(pm))
final AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.setAppInfo(info)
.addTitle(info.loadLabel(pm))
.addRank(wordDiff)
.addRank(getRank(wordDiff))
.addBreadcrumbs(getBreadCrumb())
.addPayload(new IntentPayload(intent));
results.add(builder.build());
@@ -120,7 +119,7 @@ public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
}
@Override
protected void onDiscardResult(List<SearchResult> result) {
protected void onDiscardResult(List<? extends SearchResult> result) {
}
@@ -200,4 +199,16 @@ public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
}
return mBreadcrumb;
}
/**
* A temporary ranking scheme for installed apps.
* @param wordDiff difference between query length and app name length.
* @return the ranking.
*/
private int getRank(int wordDiff) {
if (wordDiff < 6) {
return 3;
}
return 4;
}
}

View File

@@ -31,7 +31,7 @@ import java.util.List;
/**
* Loader for recently searched queries.
*/
public class SavedQueryLoader extends AsyncLoader<List<SearchResult>> {
public class SavedQueryLoader extends AsyncLoader<List<? extends SearchResult>> {
// Max number of proposed suggestions
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -45,12 +45,12 @@ public class SavedQueryLoader extends AsyncLoader<List<SearchResult>> {
}
@Override
protected void onDiscardResult(List<SearchResult> result) {
protected void onDiscardResult(List<? extends SearchResult> result) {
}
@Override
public List<SearchResult> loadInBackground() {
public List<? extends SearchResult> loadInBackground() {
Cursor cursor = mDatabase.query(IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES /* table */,
new String[]{SavedQueriesColumns.QUERY} /* columns */,
null /* selection */,

View File

@@ -65,13 +65,14 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider {
@Override
public DatabaseResultLoader getDatabaseSearchLoader(Context context, String query) {
return new DatabaseResultLoader(context, query);
return new DatabaseResultLoader(context, query, getSiteMapManager());
}
@Override
public InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query) {
return new InstalledAppResultLoader(
context, new PackageManagerWrapperImpl(context.getPackageManager()), query);
context, new PackageManagerWrapperImpl(context.getPackageManager()), query,
getSiteMapManager());
}
@Override

View File

@@ -41,9 +41,11 @@ import com.android.settings.core.instrumentation.MetricsFeatureProvider;
import com.android.settings.overlay.FeatureFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class SearchFragment extends InstrumentedFragment implements
SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<SearchResult>> {
SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<? extends SearchResult>>
{
private static final String TAG = "SearchFragment";
// State values
@@ -56,6 +58,11 @@ public class SearchFragment extends InstrumentedFragment implements
private static final int LOADER_ID_DATABASE = 1;
private static final int LOADER_ID_INSTALLED_APPS = 2;
private static final int NUM_QUERY_LOADERS = 2;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
AtomicInteger mUnfinishedLoadersCount = new AtomicInteger(NUM_QUERY_LOADERS);;
// Logging
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static final String RESULT_CLICK_COUNT = "settings_search_result_click_count";
@@ -69,9 +76,13 @@ public class SearchFragment extends InstrumentedFragment implements
private boolean mNeverEnteredQuery = true;
private int mResultClickCount;
private MetricsFeatureProvider mMetricsFeatureProvider;
private SearchFeatureProvider mSearchFeatureProvider;
private SearchResultsAdapter mSearchAdapter;
@VisibleForTesting (otherwise = VisibleForTesting.PRIVATE)
SearchFeatureProvider mSearchFeatureProvider;
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
SearchResultsAdapter mSearchAdapter;
private RecyclerView mResultsRecyclerView;
private SearchView mSearchView;
@@ -92,7 +103,9 @@ public class SearchFragment extends InstrumentedFragment implements
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
mSearchAdapter = new SearchResultsAdapter(this);
final LoaderManager loaderManager = getLoaderManager();
if (savedInstanceState != null) {
mQuery = savedInstanceState.getString(STATE_QUERY);
mNeverEnteredQuery = savedInstanceState.getBoolean(STATE_NEVER_ENTERED_QUERY);
@@ -181,7 +194,7 @@ public class SearchFragment extends InstrumentedFragment implements
}
@Override
public Loader<List<SearchResult>> onCreateLoader(int id, Bundle args) {
public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
final Activity activity = getActivity();
switch (id) {
@@ -197,12 +210,17 @@ public class SearchFragment extends InstrumentedFragment implements
}
@Override
public void onLoadFinished(Loader<List<SearchResult>> loader, List<SearchResult> data) {
mSearchAdapter.mergeResults(data, loader.getClass().getName());
public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
List<? extends SearchResult> data) {
mSearchAdapter.addResultsToMap(data, loader.getClass().getName());
if (mUnfinishedLoadersCount.decrementAndGet() == 0) {
mSearchAdapter.mergeResults();
}
}
@Override
public void onLoaderReset(Loader<List<SearchResult>> loader) {
public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
}
public void onSearchResultClicked() {
@@ -217,6 +235,7 @@ public class SearchFragment extends InstrumentedFragment implements
private void restartLoaders() {
final LoaderManager loaderManager = getLoaderManager();
mUnfinishedLoadersCount.set(NUM_QUERY_LOADERS);
loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */);
loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */);
}

View File

@@ -26,6 +26,12 @@ import java.util.Objects;
*/
public class SearchResult implements Comparable<SearchResult> {
/**
* Defines the max rank for a search result to be considered as ranked. Results with ranks
* higher than this have no guarantee for sorting order.
*/
public static final int MAX_RANK = 9;
/**
* The title of the result and main text displayed.
* Intent Results: Displays as the primary
@@ -74,7 +80,7 @@ public class SearchResult implements Comparable<SearchResult> {
*/
public final long stableId;
private SearchResult(Builder builder) {
protected SearchResult(Builder builder) {
title = builder.mTitle;
summary = builder.mSummary;
breadcrumbs = builder.mBreadcrumbs;
@@ -116,19 +122,19 @@ public class SearchResult implements Comparable<SearchResult> {
return this;
}
public Builder addRank(int rank) {
public Builder addRank(int rank) {
if (rank >= 0 && rank <= 9) {
mRank = rank;
}
return this;
}
public Builder addIcon(Drawable icon) {
public Builder addIcon(Drawable icon) {
mIcon = icon;
return this;
}
public Builder addPayload(ResultPayload payload) {
public Builder addPayload(ResultPayload payload) {
mResultPayload = payload;
return this;
}
@@ -143,4 +149,4 @@ public class SearchResult implements Comparable<SearchResult> {
return new SearchResult(this);
}
}
}
}

View File

@@ -17,6 +17,7 @@
package com.android.settings.search2;
import android.content.Context;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView.Adapter;
import android.util.ArrayMap;
@@ -31,10 +32,13 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.android.settings.search2.SearchResult.MAX_RANK;
public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
private final List<SearchResult> mSearchResults;
private final Map<String, List<SearchResult>> mResultsMap;
private final SearchFragment mFragment;
private Map<String, List<? extends SearchResult>> mResultsMap;
public SearchResultsAdapter(SearchFragment fragment) {
mFragment = fragment;
@@ -84,15 +88,56 @@ public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
return mSearchResults.size();
}
public void mergeResults(List<SearchResult> freshResults, String loaderClassName) {
/**
* Store the results from each of the loaders to be merged when all loaders are finished.
* @param freshResults are the results from the loader.
* @param loaderClassName class name of the loader.
*/
@MainThread
public void addResultsToMap(List<? extends SearchResult> freshResults,
String loaderClassName) {
if (freshResults == null) {
return;
}
mResultsMap.put(loaderClassName, freshResults);
final int oldSize = mSearchResults.size();
mSearchResults.addAll(freshResults);
final int newSize = mSearchResults.size();
notifyItemRangeInserted(oldSize, newSize - oldSize);
}
/**
* Merge the results from each of the loaders into one list for the adapter.
* Prioritizes results from the local database over installed apps.
*/
public void mergeResults() {
final List<? extends SearchResult> databaseResults = mResultsMap
.get(DatabaseResultLoader.class.getName());
final List<? extends SearchResult> installedAppResults = mResultsMap
.get(InstalledAppResultLoader.class.getName());
final int dbSize = (databaseResults != null) ? databaseResults.size() : 0;
final int appSize = (installedAppResults != null) ? installedAppResults.size() : 0;
final List<SearchResult> results = new ArrayList<>(dbSize + appSize);
int dbIndex = 0;
int appIndex = 0;
int rank = 1;
while (rank <= MAX_RANK) {
while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) {
results.add(databaseResults.get(dbIndex++));
}
while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
results.add(installedAppResults.get(appIndex++));
}
rank ++;
}
while (dbIndex < dbSize) {
results.add(databaseResults.get(dbIndex++));
}
while (appIndex < appSize) {
results.add(installedAppResults.get(appIndex++));
}
mSearchResults.addAll(results);
notifyDataSetChanged();
}
public void clearResults() {

View File

@@ -16,6 +16,9 @@
package com.android.settings.search2;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
@@ -52,8 +55,14 @@ public abstract class SearchViewHolder extends RecyclerView.ViewHolder {
summaryView.setText(result.summary);
summaryView.setVisibility(View.VISIBLE);
}
iconView.setImageDrawable(result.icon);
if (result.icon == null) {
if (result instanceof AppSearchResult) {
AppSearchResult appResult = (AppSearchResult) result;
PackageManager pm = fragment.getActivity().getPackageManager();
iconView.setImageDrawable(appResult.info.loadIcon(pm));
} else if (result.icon != null) {
iconView.setImageDrawable(result.icon);
} else {
iconView.setBackgroundResource(R.drawable.empty_icon);
}
bindBreadcrumbView(result);