Merge "Fix a memory leak caused by dashboard freature provider."
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3591222..9c3a810 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7413,7 +7413,7 @@
     <string name="apps_summary_example">24 apps installed</string>
 
     <!-- Summary of storage usage [CHAR LIMIT=NONE] -->
-    <string name="storage_summary"><xliff:g id="size1" example="8GB">%1$s</xliff:g> of <xliff:g id="size2" example="32GB">%2$s</xliff:g> used</string>
+    <string name="storage_summary"><xliff:g id="percentage" example="54%">%1$s</xliff:g> used - <xliff:g id="free_space" example="32GB">%2$s</xliff:g> free</string>
 
     <!-- Summary of display with screen sleep timeout [CHAR LIMIT=NONE] -->
     <string name="display_summary">Sleep after <xliff:g id="timeout_description" example="10 minutes">%1$s</xliff:g> of inactivity</string>
diff --git a/src/com/android/settings/DevelopmentSettings.java b/src/com/android/settings/DevelopmentSettings.java
index 7b5bab5..b90466e 100644
--- a/src/com/android/settings/DevelopmentSettings.java
+++ b/src/com/android/settings/DevelopmentSettings.java
@@ -1847,7 +1847,7 @@
         default:
             break;
         }
-        if (index >= 0) {
+        if (index >= 0 && mBluetoothSelectA2dpCodec != null) {
             summaries = resources.getStringArray(R.array.bluetooth_a2dp_codec_summaries);
             streaming = resources.getString(R.string.bluetooth_select_a2dp_codec_streaming_label, summaries[index]);
             mBluetoothSelectA2dpCodec.setSummary(streaming);
@@ -1874,7 +1874,7 @@
         default:
             break;
         }
-        if (index >= 0) {
+        if (index >= 0 && mBluetoothSelectA2dpSampleRate != null) {
             summaries = resources.getStringArray(R.array.bluetooth_a2dp_codec_sample_rate_summaries);
             streaming = resources.getString(R.string.bluetooth_select_a2dp_codec_streaming_label, summaries[index]);
              mBluetoothSelectA2dpSampleRate.setSummary(streaming);
@@ -1896,7 +1896,7 @@
         default:
             break;
         }
-        if (index >= 0) {
+        if (index >= 0 && mBluetoothSelectA2dpBitsPerSample != null) {
             summaries = resources.getStringArray(R.array.bluetooth_a2dp_codec_bits_per_sample_summaries);
             streaming = resources.getString(R.string.bluetooth_select_a2dp_codec_streaming_label, summaries[index]);
             mBluetoothSelectA2dpBitsPerSample.setSummary(streaming);
@@ -1915,7 +1915,7 @@
         default:
             break;
         }
-        if (index >= 0) {
+        if (index >= 0 && mBluetoothSelectA2dpChannelMode != null) {
             summaries = resources.getStringArray(R.array.bluetooth_a2dp_codec_channel_mode_summaries);
             streaming = resources.getString(R.string.bluetooth_select_a2dp_codec_streaming_label, summaries[index]);
              mBluetoothSelectA2dpChannelMode.setSummary(streaming);
@@ -1941,7 +1941,7 @@
             index = -1;
             break;
         }
-        if (index >= 0) {
+        if (index >= 0 && mBluetoothSelectA2dpLdacPlaybackQuality != null) {
             summaries = resources.getStringArray(R.array.bluetooth_a2dp_codec_ldac_playback_quality_summaries);
             streaming = resources.getString(R.string.bluetooth_select_a2dp_codec_streaming_label, summaries[index]);
             mBluetoothSelectA2dpLdacPlaybackQuality.setSummary(streaming);
diff --git a/src/com/android/settings/deviceinfo/StorageSettings.java b/src/com/android/settings/deviceinfo/StorageSettings.java
index b79560a..cf70429 100644
--- a/src/com/android/settings/deviceinfo/StorageSettings.java
+++ b/src/com/android/settings/deviceinfo/StorageSettings.java
@@ -19,7 +19,6 @@
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
-import android.app.DialogFragment;
 import android.app.Fragment;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -59,10 +58,10 @@
 import com.android.settingslib.drawer.SettingsDrawerActivity;
 
 import java.io.File;
+import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 
 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
 
@@ -513,10 +512,13 @@
     private static class SummaryProvider implements SummaryLoader.SummaryProvider {
         private final Context mContext;
         private final SummaryLoader mLoader;
+        private final StorageManagerVolumeProvider mStorageManagerVolumeProvider;
 
         private SummaryProvider(Context context, SummaryLoader loader) {
             mContext = context;
             mLoader = loader;
+            final StorageManager storageManager = mContext.getSystemService(StorageManager.class);
+            mStorageManagerVolumeProvider = new StorageManagerVolumeProvider(storageManager);
         }
 
         @Override
@@ -528,13 +530,13 @@
 
         private void updateSummary() {
             // TODO: Register listener.
-            final StorageManager storageManager = mContext.getSystemService(StorageManager.class);
-            PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo(
-                    new StorageManagerVolumeProvider(storageManager));
-            long privateUsedBytes = info.totalBytes - info.freeBytes;
+            final NumberFormat percentageFormat = NumberFormat.getPercentInstance();
+            final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo(
+                    mStorageManagerVolumeProvider);
+            double privateUsedBytes = info.totalBytes - info.freeBytes;
             mLoader.setSummary(this, mContext.getString(R.string.storage_summary,
-                    Formatter.formatFileSize(mContext, privateUsedBytes),
-                    Formatter.formatFileSize(mContext, info.totalBytes)));
+                    percentageFormat.format(privateUsedBytes / info.totalBytes),
+                    Formatter.formatFileSize(mContext, info.freeBytes)));
         }
     }
 
diff --git a/src/com/android/settings/search2/AppSearchResult.java b/src/com/android/settings/search2/AppSearchResult.java
new file mode 100644
index 0000000..111f775
--- /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 1f032a0..854b8dd 100644
--- a/src/com/android/settings/search2/DatabaseResultLoader.java
+++ b/src/com/android/settings/search2/DatabaseResultLoader.java
@@ -36,7 +36,7 @@
 /**
  * AsyncTask to retrieve Settings, First party app and any intent based results.
  */
-public class DatabaseResultLoader extends AsyncLoader<List<SearchResult>> {
+public class DatabaseResultLoader extends AsyncLoader<List<? extends SearchResult>> {
     private static final String LOG = "DatabaseResultLoader";
 
     /* These indices are used to match the columns of the this loader's SELECT statement.
@@ -98,26 +98,25 @@
     private static final int[] BASE_RANKS = {1, 4, 7};
 
     private final String mQueryText;
-    private final SQLiteDatabase mDatabase;
+    private final Context mContext;
     private final CursorToSearchResultConverter mConverter;
     private final SiteMapManager mSiteMapManager;
 
-    public DatabaseResultLoader(Context context, String queryText) {
+    public DatabaseResultLoader(Context context, String queryText, SiteMapManager mapManager) {
         super(context);
-        mSiteMapManager = FeatureFactory.getFactory(context)
-                .getSearchFeatureProvider().getSiteMapManager();
-        mDatabase = IndexDatabaseHelper.getInstance(context).getReadableDatabase();
+        mSiteMapManager = mapManager;
+        mContext = context;
         mQueryText = cleanQuery(queryText);
         mConverter = new CursorToSearchResultConverter(context, mQueryText);
     }
 
     @Override
-    protected void onDiscardResult(List<SearchResult> result) {
+    protected void onDiscardResult(List<? extends SearchResult> result) {
         // TODO Search
     }
 
     @Override
-    public List<SearchResult> loadInBackground() {
+    public List<? extends SearchResult> loadInBackground() {
         if (mQueryText == null || mQueryText.isEmpty()) {
             return null;
         }
@@ -144,7 +143,9 @@
         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 e60598d..c6a1b82 100644
--- a/src/com/android/settings/search2/InstalledAppResultLoader.java
+++ b/src/com/android/settings/search2/InstalledAppResultLoader.java
@@ -42,7 +42,7 @@
 /**
  * Search loader for installed apps.
  */
-public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
+public class InstalledAppResultLoader extends AsyncLoader<List<? extends SearchResult>> {
 
     private static final int NAME_NO_MATCH = -1;
     private static final Intent LAUNCHER_PROBE = new Intent(Intent.ACTION_MAIN)
@@ -56,18 +56,17 @@
 
 
     public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper,
-            String query) {
+            String query, SiteMapManager mapManager) {
         super(context);
-        mSiteMapManager = FeatureFactory.getFactory(context)
-                .getSearchFeatureProvider().getSiteMapManager();
+        mSiteMapManager = mapManager;
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
         mPackageManager = pmWrapper;
         mQuery = query;
     }
 
     @Override
-    public List<SearchResult> loadInBackground() {
-        final List<SearchResult> results = new ArrayList<>();
+    public List<? extends SearchResult> loadInBackground() {
+        final List<AppSearchResult> results = new ArrayList<>();
         final PackageManager pm = mPackageManager.getPackageManager();
 
         for (UserInfo user : getUsersToCount()) {
@@ -90,10 +89,10 @@
                         .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 @@
     }
 
     @Override
-    protected void onDiscardResult(List<SearchResult> result) {
+    protected void onDiscardResult(List<? extends SearchResult> result) {
 
     }
 
@@ -200,4 +199,16 @@
         }
         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 b034b44..245842c 100644
--- a/src/com/android/settings/search2/SavedQueryLoader.java
+++ b/src/com/android/settings/search2/SavedQueryLoader.java
@@ -31,7 +31,7 @@
 /**
  * Loader for recently searched queries.
  */
-public class SavedQueryLoader extends AsyncLoader<List<SearchResult>> {
+public class SavedQueryLoader extends AsyncLoader<List<? extends SearchResult>> {
 
     // Max number of proposed suggestions
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
@@ -45,12 +45,12 @@
     }
 
     @Override
-    protected void onDiscardResult(List<SearchResult> result) {
+    protected void onDiscardResult(List<? extends SearchResult> result) {
 
     }
 
     @Override
-    public List<SearchResult> loadInBackground() {
+    public List<? extends SearchResult> loadInBackground() {
         Cursor cursor = mDatabase.query(IndexDatabaseHelper.Tables.TABLE_SAVED_QUERIES /* table */,
                 new String[]{SavedQueriesColumns.QUERY} /* columns */,
                 null /* selection */,
diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
index b575b15..b161686 100644
--- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java
+++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
@@ -65,13 +65,14 @@
 
     @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 b688a45..a1233b7 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.overlay.FeatureFactory;
 
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class SearchFragment extends InstrumentedFragment implements
-        SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<SearchResult>> {
+        SearchView.OnQueryTextListener, LoaderManager.LoaderCallbacks<List<? extends SearchResult>>
+{
     private static final String TAG = "SearchFragment";
 
     // State values
@@ -56,6 +58,11 @@
     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 @@
     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 @@
         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 @@
     }
 
     @Override
-    public Loader<List<SearchResult>> onCreateLoader(int id, Bundle args) {
+    public Loader<List<? extends SearchResult>> onCreateLoader(int id, Bundle args) {
         final Activity activity = getActivity();
 
         switch (id) {
@@ -197,12 +210,17 @@
     }
 
     @Override
-    public void onLoadFinished(Loader<List<SearchResult>> loader, List<SearchResult> data) {
-        mSearchAdapter.mergeResults(data, loader.getClass().getName());
+    public void onLoadFinished(Loader<List<? extends SearchResult>> loader,
+            List<? extends SearchResult> data) {
+        mSearchAdapter.addResultsToMap(data, loader.getClass().getName());
+
+        if (mUnfinishedLoadersCount.decrementAndGet() == 0) {
+            mSearchAdapter.mergeResults();
+        }
     }
 
     @Override
-    public void onLoaderReset(Loader<List<SearchResult>> loader) {
+    public void onLoaderReset(Loader<List<? extends SearchResult>> loader) {
     }
 
     public void onSearchResultClicked() {
@@ -217,6 +235,7 @@
 
     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 e817a9d..37559aa 100644
--- a/src/com/android/settings/search2/SearchResult.java
+++ b/src/com/android/settings/search2/SearchResult.java
@@ -27,6 +27,12 @@
 public class SearchResult implements Comparable<SearchResult> {
 
     /**
+     * Defines the max rank for a search result to be considered as ranked. Results with ranks
+     * higher than this have no guarantee for sorting order.
+     */
+    public static final int MAX_RANK  = 9;
+
+    /**
      * The title of the result and main text displayed.
      * Intent Results: Displays as the primary
      */
@@ -74,7 +80,7 @@
      */
     public final long stableId;
 
-    private SearchResult(Builder builder) {
+    protected SearchResult(Builder builder) {
         title = builder.mTitle;
         summary = builder.mSummary;
         breadcrumbs = builder.mBreadcrumbs;
@@ -116,19 +122,19 @@
             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 @@
             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 999a485..afe432a 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.List;
 import java.util.Map;
 
+import static com.android.settings.search2.SearchResult.MAX_RANK;
+
 public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
+
     private final List<SearchResult> mSearchResults;
-    private final Map<String, List<SearchResult>> mResultsMap;
     private final SearchFragment mFragment;
+    private Map<String, List<? extends SearchResult>> mResultsMap;
 
     public SearchResultsAdapter(SearchFragment fragment) {
         mFragment = fragment;
@@ -84,15 +88,56 @@
         return mSearchResults.size();
     }
 
-    public void mergeResults(List<SearchResult> freshResults, String loaderClassName) {
+    /**
+     * Store the results from each of the loaders to be merged when all loaders are finished.
+     * @param freshResults are the results from the loader.
+     * @param loaderClassName class name of the loader.
+     */
+    @MainThread
+    public void addResultsToMap(List<? extends SearchResult> freshResults,
+            String loaderClassName) {
         if (freshResults == null) {
             return;
         }
         mResultsMap.put(loaderClassName, freshResults);
-        final int oldSize = mSearchResults.size();
-        mSearchResults.addAll(freshResults);
-        final int newSize = mSearchResults.size();
-        notifyItemRangeInserted(oldSize, newSize - oldSize);
+    }
+
+    /**
+     * Merge the results from each of the loaders into one list for the adapter.
+     * Prioritizes results from the local database over installed apps.
+     */
+    public void mergeResults() {
+        final List<? extends SearchResult> databaseResults = mResultsMap
+                .get(DatabaseResultLoader.class.getName());
+        final List<? extends SearchResult> installedAppResults = mResultsMap
+                .get(InstalledAppResultLoader.class.getName());
+        final int dbSize = (databaseResults != null) ? databaseResults.size() : 0;
+        final int appSize = (installedAppResults != null) ? installedAppResults.size() : 0;
+        final List<SearchResult> results = new ArrayList<>(dbSize + appSize);
+
+        int dbIndex = 0;
+        int appIndex = 0;
+        int rank = 1;
+
+        while (rank <= MAX_RANK) {
+            while ((dbIndex < dbSize) && (databaseResults.get(dbIndex).rank == rank)) {
+                results.add(databaseResults.get(dbIndex++));
+            }
+            while ((appIndex < appSize) && (installedAppResults.get(appIndex).rank == rank)) {
+                results.add(installedAppResults.get(appIndex++));
+            }
+            rank ++;
+        }
+
+        while (dbIndex < dbSize) {
+            results.add(databaseResults.get(dbIndex++));
+        }
+        while (appIndex < appSize) {
+            results.add(installedAppResults.get(appIndex++));
+        }
+
+        mSearchResults.addAll(results);
+        notifyDataSetChanged();
     }
 
     public void clearResults() {
diff --git a/src/com/android/settings/search2/SearchViewHolder.java b/src/com/android/settings/search2/SearchViewHolder.java
index 0b10b06..e369391 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 @@
             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/deviceinfo/SerialNumberPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/SerialNumberPreferenceControllerTest.java
index 72d6609..1b87e6c 100644
--- a/tests/robotests/src/com/android/settings/deviceinfo/SerialNumberPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/deviceinfo/SerialNumberPreferenceControllerTest.java
@@ -20,6 +20,7 @@
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceScreen;
 
+import com.android.settings.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
 
 import org.junit.Before;
@@ -27,7 +28,6 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -39,7 +39,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-@RunWith(RobolectricTestRunner.class)
+@RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class SerialNumberPreferenceControllerTest {
 
diff --git a/tests/robotests/src/com/android/settings/deviceinfo/StorageSettingsTest.java b/tests/robotests/src/com/android/settings/deviceinfo/StorageSettingsTest.java
new file mode 100644
index 0000000..84d3b98
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/deviceinfo/StorageSettingsTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.deviceinfo;
+
+
+import android.app.Activity;
+import android.icu.text.NumberFormat;
+import android.os.storage.VolumeInfo;
+import android.text.format.Formatter;
+
+import com.android.settings.R;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.dashboard.SummaryLoader;
+import com.android.settingslib.deviceinfo.StorageManagerVolumeProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class StorageSettingsTest {
+
+    @Mock
+    private StorageManagerVolumeProvider mStorageManagerVolumeProvider;
+    @Mock
+    private Activity mActivity;
+
+    private List<VolumeInfo> mVolumes;
+
+    private StorageSettings mSettings;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mVolumes = new ArrayList<>();
+        mVolumes.add(mock(VolumeInfo.class, RETURNS_DEEP_STUBS));
+        mSettings = new StorageSettings();
+        when(mStorageManagerVolumeProvider.getVolumes()).thenReturn(mVolumes);
+    }
+
+    @Test
+    public void updateSummary_shouldDisplayUsedPercentAndFreeSpace() {
+        final SummaryLoader loader = mock(SummaryLoader.class);
+        final SummaryLoader.SummaryProvider provider =
+                StorageSettings.SUMMARY_PROVIDER_FACTORY.createSummaryProvider(mActivity, loader);
+        final VolumeInfo volumeInfo = mVolumes.get(0);
+        when(volumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
+        when(volumeInfo.getPath().getTotalSpace()).thenReturn(500L);
+        when(volumeInfo.getPath().getFreeSpace()).thenReturn(0L);
+
+        ReflectionHelpers.setField(
+                provider, "mStorageManagerVolumeProvider", mStorageManagerVolumeProvider);
+        ReflectionHelpers.setField(provider, "mContext", RuntimeEnvironment.application);
+
+        provider.setListening(true);
+
+        final String percentage = NumberFormat.getPercentInstance().format(1);
+        final String freeSpace = Formatter.formatFileSize(RuntimeEnvironment.application, 0);
+        verify(loader).setSummary(provider,
+                RuntimeEnvironment.application.getString(
+                        R.string.storage_summary, percentage, freeSpace));
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java b/tests/robotests/src/com/android/settings/search/DatabaseResultLoaderTest.java
index 6368fcb..26b231e 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 @@
 
     @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 3627898..fdcea6f 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 @@
 
 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 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.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 @@
     @Test
     public void testSingleSourceMerge_ExactCopyReturned() {
         ArrayList<SearchResult> intentResults = getIntentSampleResults();
-        mAdapter.mergeResults(intentResults, mLoaderClassName);
-
-        List<SearchResult> updatedResults = mAdapter.getSearchResults();
-        assertThat(updatedResults).containsAllIn(intentResults);
-    }
-
-    @Test
-    public void testDuplicateSourceMerge_ExactCopyReturned() {
-        ArrayList<SearchResult> intentResults = getIntentSampleResults();
-        mAdapter.mergeResults(intentResults, mLoaderClassName);
-        mAdapter.mergeResults(intentResults, mLoaderClassName);
+        mAdapter.addResultsToMap(intentResults, mLoaderClassName);
+        mAdapter.mergeResults();
 
         List<SearchResult> updatedResults = mAdapter.getSearchResults();
         assertThat(updatedResults).containsAllIn(intentResults);
@@ -111,6 +108,65 @@
         assertThat(view).isInstanceOf(InlineSwitchViewHolder.class);
     }
 
+    @Test
+    public void testEndToEndSearch_ProperResultsMerged() {
+        mAdapter.addResultsToMap(getDummyAppResults(),
+                InstalledAppResultLoader.class.getName());
+        mAdapter.addResultsToMap(getDummyDbResults(),
+                DatabaseResultLoader.class.getName());
+        mAdapter.mergeResults();
+
+        List<SearchResult> results = mAdapter.getSearchResults();
+        assertThat(results.get(0).title).isEqualTo("alpha");
+        assertThat(results.get(1).title).isEqualTo("appAlpha");
+        assertThat(results.get(2).title).isEqualTo("appBravo");
+        assertThat(results.get(3).title).isEqualTo("bravo");
+        assertThat(results.get(4).title).isEqualTo("appCharlie");
+        assertThat(results.get(5).title).isEqualTo("Charlie");
+    }
+
+    private List<SearchResult> getDummyDbResults() {
+        List<SearchResult> results = new ArrayList<>();
+        IntentPayload payload = new IntentPayload(new Intent());
+        SearchResult.Builder builder = new SearchResult.Builder();
+        builder.addPayload(payload);
+
+        builder.addTitle("alpha")
+                .addRank(1);
+        results.add(builder.build());
+
+        builder.addTitle("bravo")
+                .addRank(3);
+        results.add(builder.build());
+
+        builder.addTitle("Charlie")
+                .addRank(6);
+        results.add(builder.build());
+
+        return results;
+    }
+
+    private List<AppSearchResult> getDummyAppResults() {
+        List<AppSearchResult> results = new ArrayList<>();
+        IntentPayload payload = new IntentPayload(new Intent());
+        AppSearchResult.Builder builder = new AppSearchResult.Builder();
+        builder.addPayload(payload);
+
+        builder.addTitle("appAlpha")
+                .addRank(1);
+        results.add(builder.build());
+
+        builder.addTitle("appBravo")
+                .addRank(2);
+        results.add(builder.build());
+
+        builder.addTitle("appCharlie")
+                .addRank(4);
+        results.add(builder.build());
+
+        return results;
+    }
+
     private ArrayList<SearchResult> getIntentSampleResults() {
         ArrayList<SearchResult> sampleResults = new ArrayList<>();
         ArrayList<String> breadcrumbs = new ArrayList<>();
diff --git a/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java
index c4c6788..e808946 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 @@
                         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 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 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 @@
                                 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 @@
 
         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 @@
 
         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 void query_matchingQuery_shouldRankBasedOnSimilarity() {
         final String query = "app";
 
-        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
-        final List<SearchResult> results = mLoader.loadInBackground();
+        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query,
+                mSiteMapManager);
+        final List<? extends SearchResult> results = mLoader.loadInBackground();
 
         // List is sorted by rank
-        assertThat(results.get(0).rank).isLessThan(results.get(1).rank);
-        // perfect match first
-        assertThat(results.get(0).title).isEqualTo(query);
-        // Then partial match
-        assertThat(results.get(1).title).isNotEqualTo(query);
+        assertThat(results.get(0).rank).isAtMost(results.get(1).rank);
+        assertThat(results.get(0).title).isEqualTo("app4");
+        assertThat(results.get(1).title).isEqualTo("app");
+        assertThat(results.get(2).title).isEqualTo("appBuffer");
     }
 
     @Test
@@ -197,7 +204,8 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 @@
                 .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 0000000..17cda43
--- /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<? extends SearchResult> loadInBackground() {
+        return new ArrayList<>();
+    }
+
+    @Override
+    protected void onDiscardResult(List<? extends SearchResult> 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 0000000..18d8771
--- /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<? extends SearchResult> loadInBackground() {
+        return new ArrayList<>();
+    }
+
+    @Override
+    protected void onDiscardResult(List<? extends SearchResult> result) {
+
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryLoaderTest.java
index d975f0c..5209ed9 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 @@
 
     @Test
     public void loadInBackground_shouldReturnSavedQueries() {
-        final List<SearchResult> results = mLoader.loadInBackground();
+        final List<? extends SearchResult> results = mLoader.loadInBackground();
         assertThat(results.size()).isEqualTo(SavedQueryLoader.MAX_PROPOSED_SUGGESTIONS);
         for (SearchResult result : results) {
             assertThat(result.viewType).isEqualTo(ResultPayload.PayloadType.SAVED_QUERY);
diff --git a/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java b/tests/robotests/src/com/android/settings/search2/SavedQueryRecorderTest.java
index c56ecce..5374aae 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 @@
         mRecorder.loadInBackground();
 
         final SavedQueryLoader loader = new SavedQueryLoader(mContext);
-        List<SearchResult> results = loader.loadInBackground();
+        List<? extends SearchResult> results = loader.loadInBackground();
 
         assertThat(results.size()).isEqualTo(1);
         assertThat(results.get(0).title).isEqualTo(query);
diff --git a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
index 2d4ac54..2a10078 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.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.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 @@
     private DatabaseResultLoader mDatabaseResultLoader;
     @Mock
     private InstalledAppResultLoader mInstalledAppResultLoader;
+
     @Mock
     private SavedQueryLoader mSavedQueryLoader;
 
@@ -65,8 +74,13 @@
     @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 @@
                 .thenReturn(mInstalledAppResultLoader);
         when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
                 .thenReturn(mSavedQueryLoader);
-    }
 
-    @Test
-    public void screenRotate_shouldPersistQuery() {
         final Bundle bundle = new Bundle();
         final String testQuery = "test";
         ActivityController<SearchActivity> activityController =
@@ -102,6 +113,15 @@
 
     @Test
     public void screenRotateEmptyString_ShouldNotCrash() {
+        when(mFeatureFactory.searchFeatureProvider
+                .getDatabaseSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mDatabaseResultLoader);
+        when(mFeatureFactory.searchFeatureProvider
+                .getInstalledAppSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mInstalledAppResultLoader);
+        when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
+                .thenReturn(mSavedQueryLoader);
+
         final Bundle bundle = new Bundle();
         ActivityController<SearchActivity> activityController =
                 Robolectric.buildActivity(SearchActivity.class);
@@ -124,6 +144,15 @@
 
     @Test
     public void queryTextChange_shouldTriggerLoader() {
+        when(mFeatureFactory.searchFeatureProvider
+                .getDatabaseSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mDatabaseResultLoader);
+        when(mFeatureFactory.searchFeatureProvider
+                .getInstalledAppSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mInstalledAppResultLoader);
+        when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
+                .thenReturn(mSavedQueryLoader);
+
         final String testQuery = "test";
         ActivityController<SearchActivity> activityController =
                 Robolectric.buildActivity(SearchActivity.class);
@@ -148,6 +177,15 @@
 
     @Test
     public void queryTextChangeToEmpty_shouldTriggerSavedQueryLoader() {
+        when(mFeatureFactory.searchFeatureProvider
+                .getDatabaseSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mDatabaseResultLoader);
+        when(mFeatureFactory.searchFeatureProvider
+                .getInstalledAppSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mInstalledAppResultLoader);
+        when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
+                .thenReturn(mSavedQueryLoader);
+
         ActivityController<SearchActivity> activityController =
                 Robolectric.buildActivity(SearchActivity.class);
         activityController.setup();
@@ -169,6 +207,15 @@
 
     @Test
     public void updateIndex_TriggerOnCreate() {
+        when(mFeatureFactory.searchFeatureProvider
+                .getDatabaseSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mDatabaseResultLoader);
+        when(mFeatureFactory.searchFeatureProvider
+                .getInstalledAppSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mInstalledAppResultLoader);
+        when(mFeatureFactory.searchFeatureProvider.getSavedQueryLoader(any(Context.class)))
+                .thenReturn(mSavedQueryLoader);
+
         ActivityController<SearchActivity> activityController =
                 Robolectric.buildActivity(SearchActivity.class);
         activityController.setup();
@@ -178,4 +225,28 @@
         fragment.onAttach(null);
         verify(mFeatureFactory.searchFeatureProvider).updateIndex(any(Context.class));
     }
+
+    @Test
+    public void syncLoaders_MergeWhenAllLoadersDone() {
+
+        when(mFeatureFactory.searchFeatureProvider
+                .getDatabaseSearchLoader(any(Context.class), anyString()))
+                .thenReturn(new MockDBLoader(RuntimeEnvironment.application));
+        when(mFeatureFactory.searchFeatureProvider
+                .getInstalledAppSearchLoader(any(Context.class), anyString()))
+                .thenReturn(new MockAppLoader(RuntimeEnvironment.application));
+
+        ActivityController<SearchActivity> activityController =
+                Robolectric.buildActivity(SearchActivity.class);
+        activityController.setup();
+        SearchFragment fragment = (SearchFragment) spy(activityController.get().getFragmentManager()
+                .findFragmentById(R.id.main_content));
+
+        fragment.onQueryTextChange("non-empty");
+
+        Robolectric.flushForegroundThreadScheduler();
+
+        verify(fragment, times(2)).onLoadFinished(any(Loader.class), any(List.class));
+    }
+
 }