Merge "Refactor SoundSettings to use more preference controller."
diff --git a/src/com/android/settings/applications/PackageManagerWrapper.java b/src/com/android/settings/applications/PackageManagerWrapper.java
index d399115..6c783d8 100644
--- a/src/com/android/settings/applications/PackageManagerWrapper.java
+++ b/src/com/android/settings/applications/PackageManagerWrapper.java
@@ -18,6 +18,7 @@
 
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 
 import java.util.List;
@@ -29,24 +30,30 @@
  * the API version supported by Robolectric.
  */
 public interface PackageManagerWrapper {
+
+    /**
+     * Returns the real {@code PackageManager} object.
+     */
+    PackageManager getPackageManager();
+
     /**
      * Calls {@code PackageManager.getInstalledApplicationsAsUser()}.
      *
-     * @see android.content.pm.PackageManager.PackageManager#getInstalledApplicationsAsUser
+     * @see android.content.pm.PackageManager#getInstalledApplicationsAsUser
      */
     List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId);
 
     /**
      * Calls {@code PackageManager.hasSystemFeature()}.
      *
-     * @see android.content.pm.PackageManager.PackageManager#hasSystemFeature
+     * @see android.content.pm.PackageManager#hasSystemFeature
      */
     boolean hasSystemFeature(String name);
 
     /**
      * Calls {@code PackageManager.queryIntentActivitiesAsUser()}.
      *
-     * @see android.content.pm.PackageManager.PackageManager#queryIntentActivitiesAsUser
+     * @see android.content.pm.PackageManager#queryIntentActivitiesAsUser
      */
     List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId);
 }
diff --git a/src/com/android/settings/applications/PackageManagerWrapperImpl.java b/src/com/android/settings/applications/PackageManagerWrapperImpl.java
index 8966869..db1d30a 100644
--- a/src/com/android/settings/applications/PackageManagerWrapperImpl.java
+++ b/src/com/android/settings/applications/PackageManagerWrapperImpl.java
@@ -24,6 +24,7 @@
 import java.util.List;
 
 public class PackageManagerWrapperImpl implements PackageManagerWrapper {
+
     private final PackageManager mPm;
 
     public PackageManagerWrapperImpl(PackageManager pm) {
@@ -31,6 +32,11 @@
     }
 
     @Override
+    public PackageManager getPackageManager() {
+        return mPm;
+    }
+
+    @Override
     public List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId) {
         return mPm.getInstalledApplicationsAsUser(flags, userId);
     }
diff --git a/src/com/android/settings/search2/DatabaseResultLoader.java b/src/com/android/settings/search2/DatabaseResultLoader.java
index aca94b1..a4e614f 100644
--- a/src/com/android/settings/search2/DatabaseResultLoader.java
+++ b/src/com/android/settings/search2/DatabaseResultLoader.java
@@ -23,10 +23,11 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.graphics.drawable.Drawable;
 import android.support.annotation.VisibleForTesting;
+
+import com.android.settings.R;
 import com.android.settings.search.Index;
 import com.android.settings.search.IndexDatabaseHelper;
 import com.android.settings.utils.AsyncLoader;
-import com.android.settings.R;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -107,7 +108,6 @@
                 icon = mContext.getDrawable(R.drawable.ic_search_history);
             }
 
-
             SearchResult.Builder builder = new SearchResult.Builder();
             builder.addTitle(title)
                     .addSummary(summaryOn)
diff --git a/src/com/android/settings/search2/InstalledAppResultLoader.java b/src/com/android/settings/search2/InstalledAppResultLoader.java
new file mode 100644
index 0000000..449e52c
--- /dev/null
+++ b/src/com/android/settings/search2/InstalledAppResultLoader.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 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 android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.settings.applications.PackageManagerWrapper;
+import com.android.settings.utils.AsyncLoader;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Search loader for installed apps.
+ */
+public class InstalledAppResultLoader extends AsyncLoader<List<SearchResult>> {
+
+    private static final int NAME_NO_MATCH = -1;
+    private static final int NAME_EXACT_MATCH = 0;
+
+    private final String mQuery;
+    private final UserManager mUserManager;
+    private final PackageManagerWrapper mPackageManager;
+
+    public InstalledAppResultLoader(Context context, PackageManagerWrapper pmWrapper,
+            String query) {
+        super(context);
+        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+        mPackageManager = pmWrapper;
+        mQuery = query;
+    }
+
+    @Override
+    public List<SearchResult> loadInBackground() {
+        final List<SearchResult> results = new ArrayList<>();
+        final PackageManager pm = mPackageManager.getPackageManager();
+
+        for (UserInfo user : getUsersToCount()) {
+            final List<ApplicationInfo> apps =
+                    mPackageManager.getInstalledApplicationsAsUser(
+                            PackageManager.MATCH_DISABLED_COMPONENTS
+                                    | PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS
+                                    | (user.isAdmin() ? PackageManager.MATCH_ANY_USER : 0),
+                            user.id);
+            for (ApplicationInfo info : apps) {
+                if (info.isSystemApp()) {
+                    continue;
+                }
+                final CharSequence label = info.loadLabel(pm);
+                final int wordDiff = getWordDifference(label.toString(), mQuery);
+                if (wordDiff == NAME_NO_MATCH) {
+                    continue;
+                }
+                final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+                        .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))
+                        .addTitle(info.loadLabel(pm))
+                        .addRank(wordDiff)
+                        .addPayload(new IntentPayload(intent));
+                results.add(builder.build());
+            }
+        }
+        Collections.sort(results);
+        return results;
+    }
+
+    @Override
+    protected void onDiscardResult(List<SearchResult> result) {
+
+    }
+
+    private List<UserInfo> getUsersToCount() {
+        return mUserManager.getProfiles(UserHandle.myUserId());
+    }
+
+    /**
+     * Returns "difference" between appName and query string. appName must contain all
+     * characters from query, in the same order. If not, returns NAME_NO_MATCH. If they do match,
+     * returns an int value representing how different they are, NAME_EXACT_MATCH means they match
+     * perfectly, and larger values means they are less similar.
+     * <p/>
+     * Example:
+     * appName: Abcde, query: Abcde, Returns NAME_EXACT_MATCH
+     * appName: Abcde, query: ade, Returns 2
+     * appName: Abcde, query: ae, Returns 3
+     * appName: Abcde, query: ea, Returns NAME_NO_MATCH
+     * appName: Abcde, query: xyz, Returns NAME_NO_MATCH
+     */
+    private int getWordDifference(String appName, String query) {
+        if (TextUtils.isEmpty(appName) || TextUtils.isEmpty(query)) {
+            return NAME_NO_MATCH;
+        }
+        final char[] queryTokens = query.toString().toLowerCase().toCharArray();
+        final char[] valueText = appName.toLowerCase().toCharArray();
+        if (queryTokens.length > valueText.length) {
+            return NAME_NO_MATCH;
+        }
+        int i = 0;
+        int j = 0;
+        while (i < valueText.length && j < queryTokens.length) {
+            if (valueText[i++] == queryTokens[j]) {
+                j++;
+            }
+        }
+        if (j != queryTokens.length) {
+            return NAME_NO_MATCH;
+        }
+        // Use the diff in length as a proxy of how close the 2 words match. Value range from 0
+        // to infinity.
+        return valueText.length - queryTokens.length;
+    }
+}
diff --git a/src/com/android/settings/search2/IntentSearchViewHolder.java b/src/com/android/settings/search2/IntentSearchViewHolder.java
index 0b99d6e..0187c1c 100644
--- a/src/com/android/settings/search2/IntentSearchViewHolder.java
+++ b/src/com/android/settings/search2/IntentSearchViewHolder.java
@@ -15,9 +15,11 @@
  */
 package com.android.settings.search2;
 
+import android.app.Fragment;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
+
 import com.android.settings.R;
 
 /**
@@ -25,6 +27,7 @@
  * The DatabaseResultLoader is the primary use case for this ViewHolder.
  */
 public class IntentSearchViewHolder extends SearchViewHolder {
+
     public final TextView titleView;
     public final TextView summaryView;
     public final ImageView iconView;
@@ -33,12 +36,19 @@
         super(view);
         titleView = (TextView) view.findViewById(R.id.title);
         summaryView = (TextView) view.findViewById(R.id.summary);
-        iconView= (ImageView) view.findViewById(R.id.icon);
+        iconView = (ImageView) view.findViewById(R.id.icon);
     }
 
-    public void onBind(SearchResult result) {
+    @Override
+    public void onBind(Fragment fragment, SearchResult result) {
         titleView.setText(result.title);
         summaryView.setText(result.summary);
         iconView.setImageDrawable(result.icon);
+        itemView.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                fragment.startActivity(((IntentPayload) result.payload).intent);
+            }
+        });
     }
 }
diff --git a/src/com/android/settings/search2/SearchFeatureProvider.java b/src/com/android/settings/search2/SearchFeatureProvider.java
index 569a627..da29c85 100644
--- a/src/com/android/settings/search2/SearchFeatureProvider.java
+++ b/src/com/android/settings/search2/SearchFeatureProvider.java
@@ -41,4 +41,9 @@
      * Returns a new loader to search in index database.
      */
     DatabaseResultLoader getDatabaseSearchLoader(Context context, String query);
+
+    /**
+     * Returns a new loader to search installed apps.
+     */
+    InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query);
 }
diff --git a/src/com/android/settings/search2/SearchFeatureProviderImpl.java b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
index 81a41dc..7203049 100644
--- a/src/com/android/settings/search2/SearchFeatureProviderImpl.java
+++ b/src/com/android/settings/search2/SearchFeatureProviderImpl.java
@@ -19,14 +19,11 @@
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
-import android.widget.SearchView;
 import android.view.Menu;
-
 import android.view.MenuItem;
-import com.android.settings.R;
-import com.android.settings.utils.AsyncLoader;
 
-import java.util.List;
+import com.android.settings.R;
+import com.android.settings.applications.PackageManagerWrapperImpl;
 
 /**
  * FeatureProvider for the refactored search code.
@@ -51,15 +48,15 @@
         }
         String menuTitle = mContext.getString(R.string.search_menu);
         MenuItem menuItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, menuTitle)
-            .setIcon(R.drawable.abc_ic_search_api_material)
-            .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
-                @Override
-                public boolean onMenuItemClick(MenuItem item) {
-                    Intent intent = new Intent(activity, SearchActivity.class);
-                    activity.startActivity(intent);
-                    return true;
-                }
-            });
+                .setIcon(R.drawable.abc_ic_search_api_material)
+                .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+                    @Override
+                    public boolean onMenuItemClick(MenuItem item) {
+                        Intent intent = new Intent(activity, SearchActivity.class);
+                        activity.startActivity(intent);
+                        return true;
+                    }
+                });
 
         menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
     }
@@ -68,4 +65,10 @@
     public DatabaseResultLoader getDatabaseSearchLoader(Context context, String query) {
         return new DatabaseResultLoader(context, query);
     }
+
+    @Override
+    public InstalledAppResultLoader getInstalledAppSearchLoader(Context context, String query) {
+        return new InstalledAppResultLoader(
+                context, new PackageManagerWrapperImpl(context.getPackageManager()), query);
+    }
 }
diff --git a/src/com/android/settings/search2/SearchFragment.java b/src/com/android/settings/search2/SearchFragment.java
index 1fb123c..fca52e9 100644
--- a/src/com/android/settings/search2/SearchFragment.java
+++ b/src/com/android/settings/search2/SearchFragment.java
@@ -46,13 +46,13 @@
     static final String STATE_QUERY = "query";
 
     // Loader IDs
-    private static final int DATABASE_LOADER_ID = 0;
+    private static final int LOADER_ID_DATABASE = 0;
+    private static final int LOADER_ID_INSTALLED_APPS = 1;
 
     @VisibleForTesting
     String mQuery;
 
     private SearchFeatureProvider mSearchFeatureProvider;
-    private DatabaseResultLoader mSearchLoader;
 
     private SearchResultsAdapter mSearchAdapter;
     private RecyclerView mResultsRecyclerView;
@@ -73,10 +73,12 @@
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setHasOptionsMenu(true);
-        mSearchAdapter = new SearchResultsAdapter();
+        mSearchAdapter = new SearchResultsAdapter(this);
         if (savedInstanceState != null) {
             mQuery = savedInstanceState.getString(STATE_QUERY);
-            getLoaderManager().initLoader(DATABASE_LOADER_ID, null, this);
+            final LoaderManager loaderManager = getLoaderManager();
+            loaderManager.initLoader(LOADER_ID_DATABASE, null, this);
+            loaderManager.initLoader(LOADER_ID_INSTALLED_APPS, null, this);
         }
         final ActionBar actionBar = getActivity().getActionBar();
         actionBar.setCustomView(makeSearchView(actionBar, mQuery));
@@ -109,7 +111,7 @@
         mSearchAdapter.clearResults();
 
         if (TextUtils.isEmpty(mQuery)) {
-            getLoaderManager().destroyLoader(DATABASE_LOADER_ID);
+            getLoaderManager().destroyLoader(LOADER_ID_DATABASE);
         } else {
             restartLoaders();
         }
@@ -127,9 +129,10 @@
         final Activity activity = getActivity();
 
         switch (id) {
-            case DATABASE_LOADER_ID:
-                mSearchLoader = mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
-                return mSearchLoader;
+            case LOADER_ID_DATABASE:
+                return mSearchFeatureProvider.getDatabaseSearchLoader(activity, mQuery);
+            case LOADER_ID_INSTALLED_APPS:
+                return mSearchFeatureProvider.getInstalledAppSearchLoader(activity, mQuery);
             default:
                 return null;
         }
@@ -137,10 +140,6 @@
 
     @Override
     public void onLoadFinished(Loader<List<SearchResult>> loader, List<SearchResult> data) {
-        if (data == null) {
-            return;
-        }
-
         mSearchAdapter.mergeResults(data, loader.getClass().getName());
     }
 
@@ -150,7 +149,8 @@
 
     private void restartLoaders() {
         final LoaderManager loaderManager = getLoaderManager();
-        loaderManager.restartLoader(DATABASE_LOADER_ID, null /* args */, this /* callback */);
+        loaderManager.restartLoader(LOADER_ID_DATABASE, null /* args */, this /* callback */);
+        loaderManager.restartLoader(LOADER_ID_INSTALLED_APPS, null /* args */, this /* callback */);
     }
 
     private SearchView makeSearchView(ActionBar actionBar, String query) {
diff --git a/src/com/android/settings/search2/SearchResult.java b/src/com/android/settings/search2/SearchResult.java
index e483df3..9fb250f 100644
--- a/src/com/android/settings/search2/SearchResult.java
+++ b/src/com/android/settings/search2/SearchResult.java
@@ -19,11 +19,73 @@
 import android.graphics.drawable.Drawable;
 
 import java.util.ArrayList;
+import java.util.Objects;
 
 /**
- * Dataclass as an interface for all Search Results.
+ * Data class as an interface for all Search Results.
  */
 public class SearchResult implements Comparable<SearchResult> {
+
+    /**
+     * The title of the result and main text displayed.
+     * Intent Results: Displays as the primary
+     */
+    public final CharSequence title;
+
+    /**
+     * Summary / subtitle text
+     * Intent Results: Displays the text underneath the title
+     */
+    final public CharSequence summary;
+
+    /**
+     * An ordered list of the information hierarchy.
+     * Intent Results: Displayed a hierarchy of selections to reach the setting from the home screen
+     */
+    public final ArrayList<String> breadcrumbs;
+
+    /**
+     * A suggestion for the ranking of the result.
+     * Based on Settings Rank:
+     * 1 is a near perfect match
+     * 9 is the weakest match
+     * TODO subject to change
+     */
+    public final int rank;
+
+    /**
+     * Identifier for the recycler view adapter.
+     */
+    @ResultPayload.PayloadType
+    public final int viewType;
+
+    /**
+     * Metadata for the specific result types.
+     */
+    public final ResultPayload payload;
+
+    /**
+     * Result's icon.
+     */
+    public final Drawable icon;
+
+    /**
+     * Stable id for this object.
+     */
+    public final long stableId;
+
+    private SearchResult(Builder builder) {
+        title = builder.mTitle;
+        summary = builder.mSummary;
+        breadcrumbs = builder.mBreadcrumbs;
+        rank = builder.mRank;
+        icon = builder.mIcon;
+        payload = builder.mResultPayload;
+        viewType = payload.getType();
+        stableId = Objects.hash(title, summary, breadcrumbs, rank, icon, payload, viewType);
+
+    }
+
     @Override
     public int compareTo(SearchResult searchResult) {
         if (searchResult == null) {
@@ -33,19 +95,19 @@
     }
 
     public static class Builder {
-        protected String mTitle;
-        protected String mSummary;
+        protected CharSequence mTitle;
+        protected CharSequence mSummary;
         protected ArrayList<String> mBreadcrumbs;
         protected int mRank = -1;
         protected ResultPayload mResultPayload;
         protected Drawable mIcon;
 
-        public Builder addTitle(String title) {
+        public Builder addTitle(CharSequence title) {
             mTitle = title;
             return this;
         }
 
-        public Builder addSummary(String summary) {
+        public Builder addSummary(CharSequence summary) {
             mSummary = summary;
             return this;
         }
@@ -77,10 +139,6 @@
             // Check that all of the mandatory fields are set.
             if (mTitle == null) {
                 throw new IllegalArgumentException("SearchResult missing title argument");
-            } else if (mSummary == null ) {
-                throw new IllegalArgumentException("SearchResult missing summary argument");
-            } else if (mBreadcrumbs == null){
-                throw new IllegalArgumentException("SearchResult missing breadcrumbs argument");
             } else if (mRank == -1) {
                 throw new IllegalArgumentException("SearchResult missing rank argument");
             } else if (mIcon == null) {
@@ -91,56 +149,4 @@
             return new SearchResult(this);
         }
     }
-
-    /**
-     * The title of the result and main text displayed.
-     * Intent Results: Displays as the primary
-     */
-    public final String title;
-
-    /**
-     * Summary / subtitle text
-     * Intent Results: Displays the text underneath the title
-     */
-    final public String summary;
-
-    /**
-     * An ordered list of the information hierarchy.
-     * Intent Results: Displayed a hierarchy of selections to reach the setting from the home screen
-     */
-    public final ArrayList<String> breadcrumbs;
-
-    /**
-     * A suggestion for the ranking of the result.
-     * Based on Settings Rank:
-     * 1 is a near perfect match
-     * 9 is the weakest match
-     * TODO subject to change
-     */
-    public final int rank;
-
-    /**
-     * Identifier for the recycler view adapter.
-     */
-    @ResultPayload.PayloadType public final int viewType;
-
-    /**
-     * Metadata for the specific result types.
-     */
-    public final ResultPayload payload;
-
-    /**
-     * Result's icon.
-     */
-    public final Drawable icon;
-
-    private SearchResult(Builder builder) {
-        title = builder.mTitle;
-        summary = builder.mSummary;
-        breadcrumbs = builder.mBreadcrumbs;
-        rank = builder.mRank;
-        icon = builder.mIcon;
-        payload = builder.mResultPayload;
-        viewType = payload.getType();
-    }
 }
diff --git a/src/com/android/settings/search2/SearchResultsAdapter.java b/src/com/android/settings/search2/SearchResultsAdapter.java
index 62b79b3..b588496 100644
--- a/src/com/android/settings/search2/SearchResultsAdapter.java
+++ b/src/com/android/settings/search2/SearchResultsAdapter.java
@@ -33,40 +33,19 @@
 public class SearchResultsAdapter extends Adapter<SearchViewHolder> {
     private final List<SearchResult> mSearchResults;
     private final Map<String, List<SearchResult>> mResultsMap;
+    private final SearchFragment mFragment;
 
-    public SearchResultsAdapter() {
+    public SearchResultsAdapter(SearchFragment fragment) {
+        mFragment = fragment;
         mSearchResults = new ArrayList<>();
         mResultsMap = new HashMap<>();
 
         setHasStableIds(true);
     }
 
-    public void mergeResults(List<SearchResult> freshResults, String loaderClassName) {
-        if (freshResults == null) {
-            return;
-        }
-        mResultsMap.put(loaderClassName, freshResults);
-        mSearchResults.addAll(mergeMappedResults());
-        notifyDataSetChanged();
-    }
-
-    public void clearResults() {
-        mSearchResults.clear();
-        mResultsMap.clear();
-        notifyDataSetChanged();
-    }
-
-    private ArrayList<SearchResult> mergeMappedResults() {
-        ArrayList<SearchResult> mergedResults = new ArrayList<>();
-        for (String key : mResultsMap.keySet()) {
-            mergedResults.addAll(mResultsMap.get(key));
-        }
-        return mergedResults;
-    }
-
     @Override
     public SearchViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
         switch (viewType) {
             case PayloadType.INTENT:
                 View view = inflater.inflate(R.layout.search_intent_item, parent, false);
@@ -82,13 +61,12 @@
 
     @Override
     public void onBindViewHolder(SearchViewHolder holder, int position) {
-        SearchResult result = mSearchResults.get(position);
-        holder.onBind(result);
+        holder.onBind(mFragment, mSearchResults.get(position));
     }
 
     @Override
     public long getItemId(int position) {
-        return super.getItemId(position);
+        return mSearchResults.get(position).stableId;
     }
 
     @Override
@@ -101,6 +79,23 @@
         return mSearchResults.size();
     }
 
+    public void mergeResults(List<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);
+    }
+
+    public void clearResults() {
+        mSearchResults.clear();
+        mResultsMap.clear();
+        notifyDataSetChanged();
+    }
+
     @VisibleForTesting
     public List<SearchResult> getSearchResults() {
         return mSearchResults;
diff --git a/src/com/android/settings/search2/SearchViewHolder.java b/src/com/android/settings/search2/SearchViewHolder.java
index 2f500fb..45ceb38 100644
--- a/src/com/android/settings/search2/SearchViewHolder.java
+++ b/src/com/android/settings/search2/SearchViewHolder.java
@@ -15,6 +15,7 @@
  */
 package com.android.settings.search2;
 
+import android.app.Fragment;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
@@ -29,5 +30,5 @@
         super(view);
     }
 
-    public abstract void onBind(SearchResult result);
+    public abstract void onBind(Fragment fragment, SearchResult result);
 }
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java
index 2534c0b..805c7cb 100644
--- a/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java
+++ b/tests/robotests/src/com/android/settings/search/IntentSearchViewHolderTest.java
@@ -17,40 +17,50 @@
 
 package com.android.settings.search;
 
+import android.app.Fragment;
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.drawable.Drawable;
 import android.view.LayoutInflater;
 import android.view.View;
+
 import com.android.settings.R;
 import com.android.settings.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
 import com.android.settings.search2.IntentPayload;
 import com.android.settings.search2.IntentSearchViewHolder;
-import com.android.settings.search2.SearchResult.Builder;
 import com.android.settings.search2.SearchResult;
+import com.android.settings.search2.SearchResult.Builder;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowApplication;
 
 import java.util.ArrayList;
 
 import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
 
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class IntentSearchViewHolderTest {
-    private IntentSearchViewHolder mHolder;
-    private static Drawable mIcon;
 
     private static final String TITLE = "title";
     private static final String SUMMARY = "summary";
 
+    @Mock
+    private Fragment mFragment;
+    private IntentSearchViewHolder mHolder;
+    private Drawable mIcon;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         final Context context = ShadowApplication.getInstance().getApplicationContext();
         View view = LayoutInflater.from(context).inflate(R.layout.search_intent_item, null);
         mHolder = new IntentSearchViewHolder(view);
@@ -68,11 +78,13 @@
     @Test
     public void testBindViewElements_AllUpdated() {
         SearchResult result = getSearchResult();
-        mHolder.onBind(result);
+        mHolder.onBind(mFragment, result);
+        mHolder.itemView.performClick();
 
         assertThat(mHolder.titleView.getText()).isEqualTo(TITLE);
         assertThat(mHolder.summaryView.getText()).isEqualTo(SUMMARY);
         assertThat(mHolder.iconView.getDrawable()).isEqualTo(mIcon);
+        verify(mFragment).startActivity(any(Intent.class));
     }
 
     private SearchResult getSearchResult() {
@@ -81,7 +93,7 @@
                 .addSummary(SUMMARY)
                 .addRank(1)
                 .addPayload(new IntentPayload(null))
-                .addBreadcrumbs(new ArrayList<String>())
+                .addBreadcrumbs(new ArrayList<>())
                 .addIcon(mIcon);
 
         return builder.build();
diff --git a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java
index 81e9180..0756d3f 100644
--- a/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java
+++ b/tests/robotests/src/com/android/settings/search/SearchAdapterTest.java
@@ -27,6 +27,7 @@
 import com.android.settings.search2.DatabaseResultLoader;
 import com.android.settings.search2.IntentPayload;
 import com.android.settings.search2.ResultPayload;
+import com.android.settings.search2.SearchFragment;
 import com.android.settings.search2.SearchResult;
 import com.android.settings.search2.SearchResult.Builder;
 import com.android.settings.search2.SearchResultsAdapter;
@@ -34,6 +35,8 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
 import org.robolectric.annotation.Config;
 
@@ -46,14 +49,17 @@
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
 public class SearchAdapterTest {
 
+    @Mock
+    private SearchFragment mFragment;
     private SearchResultsAdapter mAdapter;
     private Context mContext;
     private String mLoaderClassName;
 
     @Before
     public void setUp() {
+        MockitoAnnotations.initMocks(this);
         mContext = Robolectric.buildActivity(Activity.class).get();
-        mAdapter = new SearchResultsAdapter();
+        mAdapter = new SearchResultsAdapter(mFragment);
         mLoaderClassName = DatabaseResultLoader.class.getName();
     }
 
@@ -62,8 +68,7 @@
         ArrayList<String> breadcrumbs = new ArrayList<>();
         final Drawable icon = mContext.getDrawable(R.drawable.ic_search_history);
         final ResultPayload payload = new IntentPayload(null);
-
-        SearchResult.Builder builder = new Builder();
+        final SearchResult.Builder builder = new Builder();
         builder.addTitle("title")
                 .addSummary("summary")
                 .addRank(1)
diff --git a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java
index c2ec49c..a0f4cc5 100644
--- a/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java
+++ b/tests/robotests/src/com/android/settings/search/SearchResultBuilderTest.java
@@ -19,23 +19,23 @@
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+
+import com.android.settings.R;
 import com.android.settings.SettingsRobolectricTestRunner;
 import com.android.settings.TestConfig;
 import com.android.settings.search2.IntentPayload;
 import com.android.settings.search2.ResultPayload;
 import com.android.settings.search2.SearchResult;
 import com.android.settings.search2.SearchResult.Builder;
-import com.android.settings.R;
-
-import java.util.ArrayList;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-
 import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowApplication;
 
+import java.util.ArrayList;
+
 import static com.google.common.truth.Truth.assertThat;
 
 @RunWith(SettingsRobolectricTestRunner.class)
@@ -100,23 +100,6 @@
     }
 
     @Test
-    public void testNoSummary_BuildSearchResultException() {
-        mBuilder.addTitle(mTitle)
-                .addRank(mRank)
-                .addBreadcrumbs(mBreadcrumbs)
-                .addIcon(mIcon)
-                .addPayload(mResultPayload);
-
-        SearchResult result = null;
-        try {
-            result = mBuilder.build();
-        } catch (IllegalArgumentException e) {
-            // passes.
-        }
-        assertThat(result).isNull();
-    }
-
-    @Test
     public void testNoRank_BuildSearchResultException() {
         mBuilder.addTitle(mTitle)
                 .addSummary(mSummary)
@@ -134,23 +117,6 @@
     }
 
     @Test
-    public void testNoBreadcrumbs_BuildSearchResultException() {
-        mBuilder.addTitle(mTitle)
-                .addSummary(mSummary)
-                .addRank(mRank)
-                .addIcon(mIcon)
-                .addPayload(mResultPayload);
-
-        SearchResult result = null;
-        try {
-            result = mBuilder.build();
-        } catch (IllegalArgumentException e) {
-            // passes.
-        }
-        assertThat(result).isNull();
-    }
-
-    @Test
     public void testNoIcon_BuildSearchResultException() {
         mBuilder.addTitle(mTitle)
                 .addSummary(mSummary)
diff --git a/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java
new file mode 100644
index 0000000..e3c2180
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search2/InstalledAppResultLoaderTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2016 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 android.content.pm.UserInfo;
+import android.os.UserManager;
+
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.TestConfig;
+import com.android.settings.applications.PackageManagerWrapper;
+import com.android.settings.testutils.ApplicationTestUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static android.content.pm.ApplicationInfo.FLAG_SYSTEM;
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class InstalledAppResultLoaderTest {
+
+
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private Context mContext;
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private PackageManagerWrapper mPackageManagerWrapper;
+    @Mock
+    private UserManager mUserManager;
+
+    private InstalledAppResultLoader mLoader;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        final List<UserInfo> infos = new ArrayList<>();
+        infos.add(new UserInfo(1, "user 1", 0));
+        when(mUserManager.getProfiles(anyInt())).thenReturn(infos);
+        when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+        when(mPackageManagerWrapper.getInstalledApplicationsAsUser(anyInt(), anyInt()))
+                .thenReturn(Arrays.asList(
+                        ApplicationTestUtils.buildInfo(0 /* uid */, "app1", FLAG_SYSTEM),
+                        ApplicationTestUtils.buildInfo(0 /* uid */, "app2", FLAG_SYSTEM),
+                        ApplicationTestUtils.buildInfo(0 /* uid */, "app3", FLAG_SYSTEM),
+                        ApplicationTestUtils.buildInfo(0 /* uid */, "app4", 0 /* flags */),
+                        ApplicationTestUtils.buildInfo(0 /* uid */, "app", 0 /* flags */)));
+    }
+
+    @Test
+    public void query_noMatchingQuery_shouldReturnEmptyResult() {
+        final String query = "abc";
+
+        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
+
+        assertThat(mLoader.loadInBackground()).isEmpty();
+    }
+
+    @Test
+    public void query_matchingQuery_shouldReturnNonSystemApps() {
+        final String query = "app";
+
+        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
+
+        assertThat(mLoader.loadInBackground().size()).isEqualTo(2);
+    }
+
+    @Test
+    public void query_matchingQuery_shouldRankBasedOnSimilarity() {
+        final String query = "app";
+
+        mLoader = new InstalledAppResultLoader(mContext, mPackageManagerWrapper, query);
+        final List<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);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
index 979b7e5..40d1ae5 100644
--- a/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/search2/SearchFragmentTest.java
@@ -47,6 +47,8 @@
     private Context mContext;
     @Mock
     private DatabaseResultLoader mDatabaseResultLoader;
+    @Mock
+    private InstalledAppResultLoader mInstalledAppResultLoader;
     private FakeFeatureFactory mFeatureFactory;
 
     @Before
@@ -54,14 +56,16 @@
         MockitoAnnotations.initMocks(this);
         FakeFeatureFactory.setupForTest(mContext);
         mFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext);
+        when(mFeatureFactory.searchFeatureProvider
+                .getDatabaseSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mDatabaseResultLoader);
+        when(mFeatureFactory.searchFeatureProvider
+                .getInstalledAppSearchLoader(any(Context.class), anyString()))
+                .thenReturn(mInstalledAppResultLoader);
     }
 
     @Test
     public void screenRotate_shouldPersistQuery() {
-        when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mDatabaseResultLoader);
-
         final Bundle bundle = new Bundle();
         final String testQuery = "test";
         ActivityController<SearchActivity> activityController =
@@ -79,14 +83,12 @@
 
         verify(mFeatureFactory.searchFeatureProvider)
                 .getDatabaseSearchLoader(any(Context.class), anyString());
+        verify(mFeatureFactory.searchFeatureProvider)
+                .getInstalledAppSearchLoader(any(Context.class), anyString());
     }
 
     @Test
     public void queryTextChange_shouldTriggerLoader() {
-        when(mFeatureFactory.searchFeatureProvider
-                .getDatabaseSearchLoader(any(Context.class), anyString()))
-                .thenReturn(mDatabaseResultLoader);
-
         final String testQuery = "test";
         ActivityController<SearchActivity> activityController =
                 Robolectric.buildActivity(SearchActivity.class);
@@ -98,5 +100,7 @@
 
         verify(mFeatureFactory.searchFeatureProvider)
                 .getDatabaseSearchLoader(any(Context.class), anyString());
+        verify(mFeatureFactory.searchFeatureProvider)
+                .getInstalledAppSearchLoader(any(Context.class), anyString());
     }
 }