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);

View File

@@ -79,124 +79,124 @@ public class DatabaseResultLoaderTest {
@Test
public void testMatchTitle() {
loader = new DatabaseResultLoader(mContext, "title");
loader = new DatabaseResultLoader(mContext, "title", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
verify(mSiteMapManager, times(2)).buildBreadCrumb(eq(mContext), anyString(), anyString());
}
@Test
public void testMatchSummary() {
loader = new DatabaseResultLoader(mContext, "summary");
loader = new DatabaseResultLoader(mContext, "summary", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
}
@Test
public void testMatchKeywords() {
loader = new DatabaseResultLoader(mContext, "keywords");
loader = new DatabaseResultLoader(mContext, "keywords", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
}
@Test
public void testMatchEntries() {
loader = new DatabaseResultLoader(mContext, "entries");
loader = new DatabaseResultLoader(mContext, "entries", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(2);
}
@Test
public void testSpecialCaseWord_MatchesNonPrefix() {
insertSpecialCase("Data usage");
loader = new DatabaseResultLoader(mContext, "usage");
loader = new DatabaseResultLoader(mContext, "usage", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseSpace_Matches() {
insertSpecialCase("space");
loader = new DatabaseResultLoader(mContext, " space ");
loader = new DatabaseResultLoader(mContext, " space ", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseDash_MatchesWordNoDash() {
insertSpecialCase("wi-fi calling");
loader = new DatabaseResultLoader(mContext, "wifi");
loader = new DatabaseResultLoader(mContext, "wifi", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseDash_MatchesWordWithDash() {
insertSpecialCase("priorités seulment");
loader = new DatabaseResultLoader(mContext, "priorités");
loader = new DatabaseResultLoader(mContext, "priorités", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseDash_MatchesWordWithoutDash() {
insertSpecialCase("priorités seulment");
loader = new DatabaseResultLoader(mContext, "priorites");
loader = new DatabaseResultLoader(mContext, "priorites", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseDash_MatchesEntireQueryWithoutDash() {
insertSpecialCase("wi-fi calling");
loader = new DatabaseResultLoader(mContext, "wifi calling");
loader = new DatabaseResultLoader(mContext, "wifi calling", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCasePrefix_MatchesPrefixOfEntry() {
insertSpecialCase("Photos");
loader = new DatabaseResultLoader(mContext, "pho");
loader = new DatabaseResultLoader(mContext, "pho", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCasePrefix_DoesNotMatchNonPrefixSubstring() {
insertSpecialCase("Photos");
loader = new DatabaseResultLoader(mContext, "hot");
loader = new DatabaseResultLoader(mContext, "hot", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(0);
}
@Test
public void testSpecialCaseMultiWordPrefix_MatchesPrefixOfEntry() {
insertSpecialCase("Apps Notifications");
loader = new DatabaseResultLoader(mContext, "Apps");
loader = new DatabaseResultLoader(mContext, "Apps", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseMultiWordPrefix_MatchesSecondWordPrefixOfEntry() {
insertSpecialCase("Apps Notifications");
loader = new DatabaseResultLoader(mContext, "Not");
loader = new DatabaseResultLoader(mContext, "Not", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfFirstEntry() {
insertSpecialCase("Apps Notifications");
loader = new DatabaseResultLoader(mContext, "pp");
loader = new DatabaseResultLoader(mContext, "pp", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(0);
}
@Test
public void testSpecialCaseMultiWordPrefix_DoesNotMatchMatchesPrefixOfSecondEntry() {
insertSpecialCase("Apps Notifications");
loader = new DatabaseResultLoader(mContext, "tion");
loader = new DatabaseResultLoader(mContext, "tion", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(0);
}
@Test
public void testSpecialCaseMultiWordPrefixWithSpecial_MatchesPrefixOfEntry() {
insertSpecialCase("Apps & Notifications");
loader = new DatabaseResultLoader(mContext, "App");
loader = new DatabaseResultLoader(mContext, "App", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}
@Test
public void testSpecialCaseMultiWordPrefixWithSpecial_MatchesPrefixOfSecondEntry() {
insertSpecialCase("Apps & Notifications");
loader = new DatabaseResultLoader(mContext, "No");
loader = new DatabaseResultLoader(mContext, "No", mSiteMapManager);
assertThat(loader.loadInBackground().size()).isEqualTo(1);
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.search;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.view.ViewGroup;
@@ -26,11 +27,14 @@ import android.widget.FrameLayout;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.search2.AppSearchResult;
import com.android.settings.search2.DatabaseResultLoader;
import com.android.settings.search2.InlineSwitchViewHolder;
import com.android.settings.search2.InstalledAppResultLoader;
import com.android.settings.search2.IntentPayload;
import com.android.settings.search2.IntentSearchViewHolder;
import com.android.settings.search2.ResultPayload;
import com.android.settings.search2.SearchActivity;
import com.android.settings.search2.SearchFragment;
import com.android.settings.search2.SearchResult;
import com.android.settings.search2.SearchResult.Builder;
@@ -46,11 +50,13 @@ import org.robolectric.Robolectric;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowViewGroup;
import org.robolectric.util.ActivityController;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -79,17 +85,8 @@ public class SearchAdapterTest {
@Test
public void testSingleSourceMerge_ExactCopyReturned() {
ArrayList<SearchResult> intentResults = getIntentSampleResults();
mAdapter.mergeResults(intentResults, mLoaderClassName);
List<SearchResult> updatedResults = mAdapter.getSearchResults();
assertThat(updatedResults).containsAllIn(intentResults);
}
@Test
public void testDuplicateSourceMerge_ExactCopyReturned() {
ArrayList<SearchResult> intentResults = getIntentSampleResults();
mAdapter.mergeResults(intentResults, mLoaderClassName);
mAdapter.mergeResults(intentResults, mLoaderClassName);
mAdapter.addResultsToMap(intentResults, mLoaderClassName);
mAdapter.mergeResults();
List<SearchResult> updatedResults = mAdapter.getSearchResults();
assertThat(updatedResults).containsAllIn(intentResults);
@@ -111,6 +108,65 @@ public class SearchAdapterTest {
assertThat(view).isInstanceOf(InlineSwitchViewHolder.class);
}
@Test
public void testEndToEndSearch_ProperResultsMerged() {
mAdapter.addResultsToMap(getDummyAppResults(),
InstalledAppResultLoader.class.getName());
mAdapter.addResultsToMap(getDummyDbResults(),
DatabaseResultLoader.class.getName());
mAdapter.mergeResults();
List<SearchResult> results = mAdapter.getSearchResults();
assertThat(results.get(0).title).isEqualTo("alpha");
assertThat(results.get(1).title).isEqualTo("appAlpha");
assertThat(results.get(2).title).isEqualTo("appBravo");
assertThat(results.get(3).title).isEqualTo("bravo");
assertThat(results.get(4).title).isEqualTo("appCharlie");
assertThat(results.get(5).title).isEqualTo("Charlie");
}
private List<SearchResult> getDummyDbResults() {
List<SearchResult> results = new ArrayList<>();
IntentPayload payload = new IntentPayload(new Intent());
SearchResult.Builder builder = new SearchResult.Builder();
builder.addPayload(payload);
builder.addTitle("alpha")
.addRank(1);
results.add(builder.build());
builder.addTitle("bravo")
.addRank(3);
results.add(builder.build());
builder.addTitle("Charlie")
.addRank(6);
results.add(builder.build());
return results;
}
private List<AppSearchResult> getDummyAppResults() {
List<AppSearchResult> results = new ArrayList<>();
IntentPayload payload = new IntentPayload(new Intent());
AppSearchResult.Builder builder = new AppSearchResult.Builder();
builder.addPayload(payload);
builder.addTitle("appAlpha")
.addRank(1);
results.add(builder.build());
builder.addTitle("appBravo")
.addRank(2);
results.add(builder.build());
builder.addTitle("appCharlie")
.addRank(4);
results.add(builder.build());
return results;
}
private ArrayList<SearchResult> getIntentSampleResults() {
ArrayList<SearchResult> sampleResults = new ArrayList<>();
ArrayList<String> breadcrumbs = new ArrayList<>();

View File

@@ -94,6 +94,8 @@ public class InstalledAppResultLoaderTest {
ApplicationTestUtils.buildInfo(0 /* uid */, "app4", 0 /* flags */,
0 /* targetSdkVersion */),
ApplicationTestUtils.buildInfo(0 /* uid */, "app", 0 /* flags */,
0 /* targetSdkVersion */),
ApplicationTestUtils.buildInfo(0 /* uid */, "appBuffer", 0 /* flags */,
0 /* targetSdkVersion */)));
}
@@ -101,7 +103,8 @@ public class InstalledAppResultLoaderTest {
public void query_noMatchingQuery_shouldReturnEmptyResult() {
final String query = "abc";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground()).isEmpty();
}
@@ -110,12 +113,13 @@ public class InstalledAppResultLoaderTest {
public void query_matchingQuery_shouldReturnNonSystemApps() {
final String query = "app";
mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query));
mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager));
when(mLoader.getContext()).thenReturn(mContext);
when(mSiteMapManager.buildBreadCrumb(eq(mContext), anyString(), anyString()))
.thenReturn(Arrays.asList(new String[]{"123"}));
assertThat(mLoader.loadInBackground().size()).isEqualTo(2);
assertThat(mLoader.loadInBackground().size()).isEqualTo(3);
verify(mSiteMapManager)
.buildBreadCrumb(eq(mContext), anyString(), anyString());
}
@@ -128,7 +132,8 @@ public class InstalledAppResultLoaderTest {
0 /* targetSdkVersion */)));
final String query = "app";
mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query));
mLoader = spy(new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager));
when(mLoader.getContext()).thenReturn(mContext);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
@@ -150,7 +155,8 @@ public class InstalledAppResultLoaderTest {
final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -167,7 +173,8 @@ public class InstalledAppResultLoaderTest {
final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground()).isEmpty();
verify(mSiteMapManager, never())
@@ -178,15 +185,15 @@ public class InstalledAppResultLoaderTest {
public void query_matchingQuery_shouldRankBasedOnSimilarity() {
final String query = "app";
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
final List<SearchResult> results = mLoader.loadInBackground();
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
final List<? extends SearchResult> results = mLoader.loadInBackground();
// List is sorted by rank
assertThat(results.get(0).rank).isLessThan(results.get(1).rank);
// perfect match first
assertThat(results.get(0).title).isEqualTo(query);
// Then partial match
assertThat(results.get(1).title).isNotEqualTo(query);
assertThat(results.get(0).rank).isAtMost(results.get(1).rank);
assertThat(results.get(0).title).isEqualTo("app4");
assertThat(results.get(1).title).isEqualTo("app");
assertThat(results.get(2).title).isEqualTo("appBuffer");
}
@Test
@@ -197,7 +204,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -210,7 +218,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(0);
}
@@ -223,7 +232,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -236,7 +246,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -249,7 +260,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -262,7 +274,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -275,7 +288,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -288,7 +302,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -301,7 +316,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(1);
}
@@ -314,7 +330,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(0);
}
@@ -327,7 +344,8 @@ public class InstalledAppResultLoaderTest {
.thenReturn(Arrays.asList(
ApplicationTestUtils.buildInfo(0 /* uid */, packageName, 0 /* flags */,
0 /* targetSdkVersion */)));
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
mSiteMapManager);
assertThat(mLoader.loadInBackground().size()).isEqualTo(0);
}

View File

@@ -0,0 +1,44 @@
/*
* 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.Context;
import java.util.ArrayList;
import java.util.List;
/**
* Mock loader to subvert the requirements of returning data while also driving the Loader
* lifecycle.
*/
class MockAppLoader extends InstalledAppResultLoader {
public MockAppLoader(Context context) {
super(context, null, "", null);
}
@Override
public List<? extends SearchResult> loadInBackground() {
return new ArrayList<>();
}
@Override
protected void onDiscardResult(List<? extends SearchResult> result) {
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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.Context;
import java.util.ArrayList;
import java.util.List;
/**
* Mock loader to subvert the requirements of returning data while also driving the Loader
* lifecycle.
*/
class MockDBLoader extends DatabaseResultLoader {
public MockDBLoader(Context context) {
super(context, "test", null);
}
@Override
public List<? extends SearchResult> loadInBackground() {
return new ArrayList<>();
}
@Override
protected void onDiscardResult(List<? extends SearchResult> result) {
}
}

View File

@@ -60,7 +60,7 @@ public class SavedQueryLoaderTest {
@Test
public void loadInBackground_shouldReturnSavedQueries() {
final List<SearchResult> results = mLoader.loadInBackground();
final List<? extends SearchResult> results = mLoader.loadInBackground();
assertThat(results.size()).isEqualTo(SavedQueryLoader.MAX_PROPOSED_SUGGESTIONS);
for (SearchResult result : results) {
assertThat(result.viewType).isEqualTo(ResultPayload.PayloadType.SAVED_QUERY);

View File

@@ -59,7 +59,7 @@ public class SavedQueryRecorderTest {
mRecorder.loadInBackground();
final SavedQueryLoader loader = new SavedQueryLoader(mContext);
List<SearchResult> results = loader.loadInBackground();
List<? extends SearchResult> results = loader.loadInBackground();
assertThat(results.size()).isEqualTo(1);
assertThat(results.get(0).title).isEqualTo(query);

View File

@@ -20,12 +20,16 @@ import android.content.Context;
import android.content.Loader;
import android.os.Bundle;
import android.os.UserManager;
import com.android.internal.logging.nano.MetricsProto;
import com.android.settings.R;
import com.android.settings.SettingsRobolectricTestRunner;
import com.android.settings.TestConfig;
import com.android.settings.dashboard.SiteMapManager;
import com.android.settings.testutils.DatabaseTestUtils;
import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,16 +37,20 @@ import org.mockito.Answers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ActivityController;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -57,6 +65,7 @@ public class SearchFragmentTest {
private DatabaseResultLoader mDatabaseResultLoader;
@Mock
private InstalledAppResultLoader mInstalledAppResultLoader;
@Mock
private SavedQueryLoader mSavedQueryLoader;
@@ -65,8 +74,13 @@ public class SearchFragmentTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
FakeFeatureFactory.setupForTest(mContext);
mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
}
@Test
public void screenRotate_shouldPersistQuery() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
@@ -75,10 +89,7 @@ public class SearchFragmentTest {
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
}
@Test
public void screenRotate_shouldPersistQuery() {
final Bundle bundle = new Bundle();
final String testQuery = "test";
ActivityController<SearchActivity> activityController =
@@ -102,6 +113,15 @@ public class SearchFragmentTest {
@Test
public void screenRotateEmptyString_ShouldNotCrash() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
final Bundle bundle = new Bundle();
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
@@ -124,6 +144,15 @@ public class SearchFragmentTest {
@Test
public void queryTextChange_shouldTriggerLoader() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
final String testQuery = "test";
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
@@ -148,6 +177,15 @@ public class SearchFragmentTest {
@Test
public void queryTextChangeToEmpty_shouldTriggerSavedQueryLoader() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
@@ -169,6 +207,15 @@ public class SearchFragmentTest {
@Test
public void updateIndex_TriggerOnCreate() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(mDatabaseResultLoader);
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(mInstalledAppResultLoader);
when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
.thenReturn(mSavedQueryLoader);
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
@@ -178,4 +225,28 @@ public class SearchFragmentTest {
fragment.onAttach(null);
verify(mFeatureFactory.searchFeatureProvider).updateIndex(any(Context.class));
}
@Test
public void syncLoaders_MergeWhenAllLoadersDone() {
when(mFeatureFactory.searchFeatureProvider
.getDatabaseSearchLoader(any(Context.class), anyString()))
.thenReturn(new MockDBLoader(RuntimeEnvironment.application));
when(mFeatureFactory.searchFeatureProvider
.getInstalledAppSearchLoader(any(Context.class), anyString()))
.thenReturn(new MockAppLoader(RuntimeEnvironment.application));
ActivityController<SearchActivity> activityController =
Robolectric.buildActivity(SearchActivity.class);
activityController.setup();
SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
.findFragmentById(R.id.main_content));
fragment.onQueryTextChange("non-empty");
Robolectric.flushForegroundThreadScheduler();
verify(fragment, times(2)).onLoadFinished(any(Loader.class), any(List.class));
}
}