From 40ce0fab758714c973945ec8f053c52076d5fc50 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Thu, 2 Feb 2017 14:55:49 -0800 Subject: [PATCH] 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 --- .../settings/search2/AppSearchResult.java | 45 +++++++++++ .../search2/DatabaseResultLoader.java | 19 ++--- .../search2/InstalledAppResultLoader.java | 31 +++++--- .../settings/search2/SavedQueryLoader.java | 6 +- .../search2/SearchFeatureProviderImpl.java | 5 +- .../settings/search2/SearchFragment.java | 33 ++++++-- .../settings/search2/SearchResult.java | 16 ++-- .../search2/SearchResultsAdapter.java | 57 +++++++++++-- .../settings/search2/SearchViewHolder.java | 13 ++- .../search/DatabaseResultLoaderTest.java | 36 ++++----- .../settings/search/SearchAdapterTest.java | 78 +++++++++++++++--- .../search2/InstalledAppResultLoaderTest.java | 66 ++++++++++------ .../settings/search2/MockAppLoader.java | 44 +++++++++++ .../settings/search2/MockDBLoader.java | 44 +++++++++++ .../search2/SavedQueryLoaderTest.java | 2 +- .../search2/SavedQueryRecorderTest.java | 2 +- .../settings/search2/SearchFragmentTest.java | 79 ++++++++++++++++++- 17 files changed, 473 insertions(+), 103 deletions(-) create mode 100644 src/com/android/settings/search2/AppSearchResult.java create mode 100644 tests/robotests/src/com/android/settings/search2/MockAppLoader.java create mode 100644 tests/robotests/src/com/android/settings/search2/MockDBLoader.java diff --git a/src/com/android/settings/search2/AppSearchResult.java b/src/com/android/settings/search2/AppSearchResult.java new file mode 100644 index 00000000000..111f7756a49 --- /dev/null +++ b/src/com/android/settings/search2/AppSearchResult.java @@ -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); + } + } +} diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java index 1f032a06878..854b8dd4619 100644 --- a/src/com/android/settings/search2/DatabaseResultLoader.java +++ b/src/com/android/settings/search2/DatabaseResultLoader.java @@ -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> { +public class DatabaseResultLoader extends AsyncLoader> { 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> { 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 result) { + protected void onDiscardResult(List result) { // TODO Search } @Override - public List loadInBackground() { + public List loadInBackground() { if (mQueryText == null || mQueryText.isEmpty()) { return null; } @@ -144,7 +143,9 @@ public class DatabaseResultLoader extends AsyncLoader> { 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); } diff --git a/src/com/android/settings/search2/InstalledAppResultLoader.java b/src/com/android/settings/search2/InstalledAppResultLoader.java index e60598d7d08..c6a1b821135 100644 --- a/src/com/android/settings/search2/InstalledAppResultLoader.java +++ b/src/com/android/settings/search2/InstalledAppResultLoader.java @@ -42,7 +42,7 @@ import java.util.List; /** * Search loader for installed apps. */ -public class InstalledAppResultLoader extends AsyncLoader> { +public class InstalledAppResultLoader extends AsyncLoader> { 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> { 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 loadInBackground() { - final List results = new ArrayList<>(); + public List loadInBackground() { + final List results = new ArrayList<>(); final PackageManager pm = mPackageManager.getPackageManager(); for (UserInfo user : getUsersToCount()) { @@ -90,10 +89,10 @@ public class InstalledAppResultLoader extends AsyncLoader> { .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> { } @Override - protected void onDiscardResult(List result) { + protected void onDiscardResult(List result) { } @@ -200,4 +199,16 @@ public class InstalledAppResultLoader extends AsyncLoader> { } 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; + } } diff --git a/src/com/android/settings/search2/SavedQueryLoader.java b/src/com/android/settings/search2/SavedQueryLoader.java index b034b443a62..245842c49e5 100644 --- a/src/com/android/settings/search2/SavedQueryLoader.java +++ b/src/com/android/settings/search2/SavedQueryLoader.java @@ -31,7 +31,7 @@ import java.util.List; /** * Loader for recently searched queries. */ -public class SavedQueryLoader extends AsyncLoader> { +public class SavedQueryLoader extends AsyncLoader> { // Max number of proposed suggestions @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -45,12 +45,12 @@ public class SavedQueryLoader extends AsyncLoader> { } @Override - protected void onDiscardResult(List result) { + protected void onDiscardResult(List result) { } @Override - public List loadInBackground() { + public List loadInBackground() { Cursor cursor = mDatabase.query(IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES /* table */, new String[]{SavedQueriesColumns.QUERY} /* columns */, null /* selection */, diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java index b575b156279..b1616863ecb 100644 --- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java @@ -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 diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java index b688a455522..a1233b764c4 100644 --- a/src/com/android/settings/search2/SearchFragment.java +++ b/src/com/android/settings/search2/SearchFragment.java @@ -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> { + SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks> +{ 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> onCreateLoader(int id, Bundle args) { + public Loader> 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> loader, List data) { - mSearchAdapter.mergeResults(data, loader.getClass().getName()); + public void onLoadFinished(Loader> loader, + List data) { + mSearchAdapter.addResultsToMap(data, loader.getClass().getName()); + + if (mUnfinishedLoadersCount.decrementAndGet() == 0) { + mSearchAdapter.mergeResults(); + } } @Override - public void onLoaderReset(Loader> loader) { + public void onLoaderReset(Loader> 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 */); } diff --git a/src/com/android/settings/search2/SearchResult.java b/src/com/android/settings/search2/SearchResult.java index e817a9d4f45..37559aa9a5f 100644 --- a/src/com/android/settings/search2/SearchResult.java +++ b/src/com/android/settings/search2/SearchResult.java @@ -26,6 +26,12 @@ import java.util.Objects; */ public class SearchResult implements Comparable { + /** + * 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 { */ 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 { 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 { return new SearchResult(this); } } -} +} \ No newline at end of file diff --git a/src/com/android/settings/search2/SearchResultsAdapter.java b/src/com/android/settings/search2/SearchResultsAdapter.java index 999a485b3da..afe432a996d 100644 --- a/src/com/android/settings/search2/SearchResultsAdapter.java +++ b/src/com/android/settings/search2/SearchResultsAdapter.java @@ -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 { + private final List mSearchResults; - private final Map> mResultsMap; private final SearchFragment mFragment; + private Map> mResultsMap; public SearchResultsAdapter(SearchFragment fragment) { mFragment = fragment; @@ -84,15 +88,56 @@ public class SearchResultsAdapter extends Adapter { return mSearchResults.size(); } - public void mergeResults(List 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 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 databaseResults = mResultsMap + .get(DatabaseResultLoader.class.getName()); + final List installedAppResults = mResultsMap + .get(InstalledAppResultLoader.class.getName()); + final int dbSize = (databaseResults != null) ? databaseResults.size() : 0; + final int appSize = (installedAppResults != null) ? installedAppResults.size() : 0; + final List 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() { diff --git a/src/com/android/settings/search2/SearchViewHolder.java b/src/com/android/settings/search2/SearchViewHolder.java index 0b10b0695d8..e36939126db 100644 --- a/src/com/android/settings/search2/SearchViewHolder.java +++ b/src/com/android/settings/search2/SearchViewHolder.java @@ -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); diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java index 6368fcb4808..26b231e0286 100644 --- a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java @@ -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); } diff --git a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java index 3627898bbeb..fdcea6f83e8 100644 --- a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java +++ b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java @@ -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 intentResults = getIntentSampleResults(); - mAdapter.mergeResults(intentResults, mLoaderClassName); - - List updatedResults = mAdapter.getSearchResults(); - assertThat(updatedResults).containsAllIn(intentResults); - } - - @Test - public void testDuplicateSourceMerge_ExactCopyReturned() { - ArrayList intentResults = getIntentSampleResults(); - mAdapter.mergeResults(intentResults, mLoaderClassName); - mAdapter.mergeResults(intentResults, mLoaderClassName); + mAdapter.addResultsToMap(intentResults, mLoaderClassName); + mAdapter.mergeResults(); List 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 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 getDummyDbResults() { + List 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 getDummyAppResults() { + List 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 getIntentSampleResults() { ArrayList sampleResults = new ArrayList<>(); ArrayList breadcrumbs = new ArrayList<>(); diff --git a/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java index c4c67882f4a..e808946799e 100644 --- a/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java @@ -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 results = mLoader.loadInBackground(); + mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query, + mSiteMapManager); + final List 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); } diff --git a/tests/robotests/src/com/android/settings/search2/MockAppLoader.java b/tests/robotests/src/com/android/settings/search2/MockAppLoader.java new file mode 100644 index 00000000000..17cda437ff7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search2/MockAppLoader.java @@ -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 loadInBackground() { + return new ArrayList<>(); + } + + @Override + protected void onDiscardResult(List result) { + + } +} diff --git a/tests/robotests/src/com/android/settings/search2/MockDBLoader.java b/tests/robotests/src/com/android/settings/search2/MockDBLoader.java new file mode 100644 index 00000000000..18d877101e8 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search2/MockDBLoader.java @@ -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 loadInBackground() { + return new ArrayList<>(); + } + + @Override + protected void onDiscardResult(List result) { + + } +} diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java index d975f0c8f97..5209ed98082 100644 --- a/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java +++ b/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java @@ -60,7 +60,7 @@ public class SavedQueryLoaderTest { @Test public void loadInBackground_shouldReturnSavedQueries() { - final List results = mLoader.loadInBackground(); + final List results = mLoader.loadInBackground(); assertThat(results.size()).isEqualTo(SavedQueryLoader.MAX_PROPOSED_SUGGESTIONS); for (SearchResult result : results) { assertThat(result.viewType).isEqualTo(ResultPayload.PayloadType.SAVED_QUERY); diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java index c56ecce537a..5374aae9aac 100644 --- a/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java +++ b/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java @@ -59,7 +59,7 @@ public class SavedQueryRecorderTest { mRecorder.loadInBackground(); final SavedQueryLoader loader = new SavedQueryLoader(mContext); - List results = loader.loadInBackground(); + List results = loader.loadInBackground(); assertThat(results.size()).isEqualTo(1); assertThat(results.get(0).title).isEqualTo(query); diff --git a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java index 2d4ac54d64d..2a10078599c 100644 --- a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java +++ b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java @@ -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 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 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 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 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 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 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)); + } + }