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