Adding market search.

Change-Id: Id41615653cd4fa76213add4595418ad0cc6e7852
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 9d04770..2d338e3 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -2132,6 +2132,15 @@
         }
     }
 
+    public void startSearchFromAllApps(View v, Intent searchIntent, String searchQuery) {
+        if (mLauncherCallbacks != null && mLauncherCallbacks.startSearchFromAllApps(searchQuery)) {
+            return;
+        }
+
+        // If not handled, then just start the provided search intent
+        startActivitySafely(v, searchIntent, null);
+    }
+
     public boolean isOnCustomContent() {
         return mWorkspace.isOnOrMovingToCustomContent();
     }
@@ -2533,6 +2542,10 @@
         if (!isAppsViewVisible()) {
             showAppsView(true /* animated */, false /* resetListToTop */,
                     true /* updatePredictedApps */, false /* focusSearchBar */);
+
+            if (mLauncherCallbacks != null) {
+                mLauncherCallbacks.onClickAllAppsButton(v);
+            }
         }
     }
 
@@ -2924,7 +2937,7 @@
         return false;
     }
 
-    @Thunk boolean startActivitySafely(View v, Intent intent, Object tag) {
+    public boolean startActivitySafely(View v, Intent intent, Object tag) {
         boolean success = false;
         if (mIsSafeModeEnabled && !Utilities.isSystemApp(this, intent)) {
             Toast.makeText(this, R.string.safemode_shortcut_error, Toast.LENGTH_SHORT).show();
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index 6618cca..e34bd57 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -77,6 +77,7 @@
     public boolean providesSearch();
     public boolean startSearch(String initialQuery, boolean selectInitialQuery,
             Bundle appSearchData, Rect sourceBounds);
+    public boolean startSearchFromAllApps(String query);
     @Deprecated
     public void startVoice();
     public boolean hasCustomContentToLeft();
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 010b2cb..e129dc6 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -16,34 +16,26 @@
 package com.android.launcher3.allapps;
 
 import android.annotation.SuppressLint;
-import android.annotation.TargetApi;
 import android.content.Context;
+import android.content.Intent;
 import android.content.res.Resources;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.drawable.InsetDrawable;
-import android.os.Build;
-import android.os.Bundle;
 import android.support.v7.widget.RecyclerView;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.widget.FrameLayout;
 import android.widget.LinearLayout;
-
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BaseContainerView;
-import com.android.launcher3.BubbleTextView;
 import com.android.launcher3.CellLayout;
-import com.android.launcher3.CheckLongPressHelper;
 import com.android.launcher3.DeleteDropTarget;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DragSource;
@@ -53,7 +45,6 @@
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherTransitionable;
 import com.android.launcher3.R;
-import com.android.launcher3.Stats;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.util.ComponentKey;
@@ -155,6 +146,7 @@
     @Thunk AllAppsSearchBarController mSearchBarController;
     private ViewGroup mSearchBarContainerView;
     private View mSearchBarView;
+    private SpannableStringBuilder mSearchQueryBuilder = null;
 
     private int mSectionNamesMargin;
     private int mNumAppsPerRow;
@@ -165,7 +157,13 @@
     // This coordinate is relative to its parent
     private final Point mIconLastTouchPos = new Point();
 
-    private SpannableStringBuilder mSearchQueryBuilder = null;
+    private View.OnClickListener mSearchClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            Intent searchIntent = (Intent) v.getTag();
+            mLauncher.startActivitySafely(v, searchIntent, null);
+        }
+    };
 
     public AllAppsContainerView(Context context) {
         this(context, null);
@@ -182,8 +180,7 @@
         mLauncher = (Launcher) context;
         mSectionNamesMargin = res.getDimensionPixelSize(R.dimen.all_apps_grid_view_start_margin);
         mApps = new AlphabeticalAppsList(context);
-        mAdapter = new AllAppsGridAdapter(context, mApps, this, mLauncher, this);
-        mAdapter.setEmptySearchText(res.getString(R.string.all_apps_loading_message));
+        mAdapter = new AllAppsGridAdapter(mLauncher, mApps, this, mLauncher, this);
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mItemDecoration = mAdapter.getItemDecoration();
@@ -615,13 +612,9 @@
     @Override
     public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
         if (apps != null) {
-            if (apps.isEmpty()) {
-                String formatStr = getResources().getString(R.string.all_apps_no_search_results);
-                mAdapter.setEmptySearchText(String.format(formatStr, query));
-            } else {
-                mAppsRecyclerView.scrollToTop();
-            }
             mApps.setOrderedFilter(apps);
+            mAdapter.setLastSearchQuery(query);
+            mAppsRecyclerView.scrollToTop();
         }
     }
 
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index e96567c..4acfc5c 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -16,14 +16,17 @@
 package com.android.launcher3.allapps;
 
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.Rect;
-import android.os.Handler;
 import android.support.v4.view.accessibility.AccessibilityRecordCompat;
 import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.net.Uri;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
@@ -34,6 +37,7 @@
 import android.widget.TextView;
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.Thunk;
@@ -58,6 +62,10 @@
     public static final int PREDICTION_ICON_VIEW_TYPE = 2;
     // The message shown when there are no filtered results
     public static final int EMPTY_SEARCH_VIEW_TYPE = 3;
+    // A divider that separates the apps list and the search market button
+    public static final int SEARCH_MARKET_DIVIDER_VIEW_TYPE = 4;
+    // The message to continue to a market search when there are no filtered results
+    public static final int SEARCH_MARKET_VIEW_TYPE = 5;
 
     /**
      * ViewHolder for each icon.
@@ -83,12 +91,12 @@
         @Override
         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
             super.onInitializeAccessibilityEvent(event);
-            if (mApps.hasNoFilteredResults()) {
-                // Disregard the no-search-results text as a list item for accessibility
-                final AccessibilityRecordCompat record = AccessibilityEventCompat
-                        .asRecord(event);
-                record.setItemCount(0);
-            }
+
+            // Ensure that we only report the number apps for accessibility not including other
+            // adapter views
+            final AccessibilityRecordCompat record = AccessibilityEventCompat
+                    .asRecord(event);
+            record.setItemCount(mApps.getNumFilteredApps());
         }
 
         @Override
@@ -115,11 +123,6 @@
 
         @Override
         public int getSpanSize(int position) {
-            if (mApps.hasNoFilteredResults()) {
-                // Empty view spans full width
-                return mAppsPerRow;
-            }
-
             switch (mApps.getAdapterItems().get(position).viewType) {
                 case AllAppsGridAdapter.ICON_VIEW_TYPE:
                 case AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE:
@@ -314,6 +317,7 @@
         }
     }
 
+    private Launcher mLauncher;
     private LayoutInflater mLayoutInflater;
     @Thunk AlphabeticalAppsList mApps;
     private GridLayoutManager mGridLayoutMgr;
@@ -326,7 +330,19 @@
     @Thunk int mPredictionBarDividerOffset;
     @Thunk int mAppsPerRow;
     @Thunk boolean mIsRtl;
-    private String mEmptySearchText;
+
+    // The text to show when there are no search results and no market search handler.
+    private String mEmptySearchMessage;
+    // The name of the market app which handles searches, to be used in the format str
+    // below when updating the search-market view.  Only needs to be loaded once.
+    private String mMarketAppName;
+    // The text to show when there is a market app which can handle a specific query, updated
+    // each time the search query changes.
+    private String mMarketSearchMessage;
+    // The intent to send off to the market app, updated each time the search query changes.
+    private Intent mMarketSearchIntent;
+    // The last query that the user entered into the search field
+    private String mLastSearchQuery;
 
     // Section drawing
     @Thunk int mSectionNamesMargin;
@@ -334,16 +350,18 @@
     @Thunk Paint mSectionTextPaint;
     @Thunk Paint mPredictedAppsDividerPaint;
 
-    public AllAppsGridAdapter(Context context, AlphabeticalAppsList apps,
+    public AllAppsGridAdapter(Launcher launcher, AlphabeticalAppsList apps,
             View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
             View.OnLongClickListener iconLongClickListener) {
-        Resources res = context.getResources();
+        Resources res = launcher.getResources();
+        mLauncher = launcher;
         mApps = apps;
+        mEmptySearchMessage = res.getString(R.string.all_apps_loading_message);
         mGridSizer = new GridSpanSizer();
-        mGridLayoutMgr = new AppsGridLayoutManager(context);
+        mGridLayoutMgr = new AppsGridLayoutManager(launcher);
         mGridLayoutMgr.setSpanSizeLookup(mGridSizer);
         mItemDecoration = new GridItemDecoration();
-        mLayoutInflater = LayoutInflater.from(context);
+        mLayoutInflater = LayoutInflater.from(launcher);
         mTouchListener = touchListener;
         mIconClickListener = iconClickListener;
         mIconLongClickListener = iconLongClickListener;
@@ -363,6 +381,14 @@
         mPredictionBarDividerOffset =
                 (-res.getDimensionPixelSize(R.dimen.all_apps_prediction_icon_bottom_padding) +
                         res.getDimensionPixelSize(R.dimen.all_apps_icon_top_bottom_padding)) / 2;
+
+        // Resolve the market app handling additional searches
+        PackageManager pm = launcher.getPackageManager();
+        ResolveInfo marketInfo = pm.resolveActivity(createMarketSearchIntent(""),
+                PackageManager.MATCH_DEFAULT_ONLY);
+        if (marketInfo != null) {
+            mMarketAppName = marketInfo.loadLabel(pm).toString();
+        }
     }
 
     /**
@@ -381,10 +407,19 @@
     }
 
     /**
-     * Sets the text to show when there are no apps.
+     * Sets the last search query that was made, used to show when there are no results and to also
+     * seed the intent for searching the market.
      */
-    public void setEmptySearchText(String query) {
-        mEmptySearchText = query;
+    public void setLastSearchQuery(String query) {
+        Resources res = mLauncher.getResources();
+        String formatStr = res.getString(R.string.all_apps_no_search_results);
+        mLastSearchQuery = query;
+        mEmptySearchMessage = String.format(formatStr, query);
+        if (mMarketAppName != null) {
+            mMarketSearchMessage = String.format(res.getString(R.string.all_apps_search_market_message),
+                    mMarketAppName);
+            mMarketSearchIntent = createMarketSearchIntent(query);
+        }
     }
 
     /**
@@ -413,9 +448,6 @@
     @Override
     public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
         switch (viewType) {
-            case EMPTY_SEARCH_VIEW_TYPE:
-                return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search, parent,
-                        false));
             case SECTION_BREAK_VIEW_TYPE:
                 return new ViewHolder(new View(parent.getContext()));
             case ICON_VIEW_TYPE: {
@@ -440,6 +472,22 @@
                 icon.setFocusable(true);
                 return new ViewHolder(icon);
             }
+            case EMPTY_SEARCH_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_empty_search,
+                        parent, false));
+            case SEARCH_MARKET_DIVIDER_VIEW_TYPE:
+                return new ViewHolder(mLayoutInflater.inflate(R.layout.all_apps_search_market_divider,
+                        parent, false));
+            case SEARCH_MARKET_VIEW_TYPE:
+                View searchMarketView = mLayoutInflater.inflate(R.layout.all_apps_search_market,
+                        parent, false);
+                searchMarketView.setOnClickListener(new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        mLauncher.startSearchFromAllApps(v, mMarketSearchIntent, mLastSearchQuery);
+                    }
+                });
+                return new ViewHolder(searchMarketView);
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -461,28 +509,44 @@
                 break;
             }
             case EMPTY_SEARCH_VIEW_TYPE:
-                TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
-                emptyViewText.setText(mEmptySearchText);
+                TextView emptyViewText = (TextView) holder.mContent;
+                emptyViewText.setText(mEmptySearchMessage);
+                break;
+            case SEARCH_MARKET_VIEW_TYPE:
+                View searchView = holder.mContent;
+                if (mMarketSearchIntent != null) {
+                    searchView.setVisibility(View.VISIBLE);
+                    searchView.setContentDescription(mMarketSearchMessage);
+                    ((TextView) searchView.findViewById(R.id.search_market_text))
+                            .setText(mMarketSearchMessage);
+                } else {
+                    searchView.setVisibility(View.GONE);
+                }
                 break;
         }
     }
 
     @Override
     public int getItemCount() {
-        if (mApps.hasNoFilteredResults()) {
-            // For the empty view
-            return 1;
-        }
         return mApps.getAdapterItems().size();
     }
 
     @Override
     public int getItemViewType(int position) {
-        if (mApps.hasNoFilteredResults()) {
-            return EMPTY_SEARCH_VIEW_TYPE;
-        }
-
         AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
         return item.viewType;
     }
+
+    /**
+     * Creates a new market search intent.
+     */
+    private Intent createMarketSearchIntent(String query) {
+        Uri marketSearchUri = Uri.parse("market://search")
+                .buildUpon()
+                .appendQueryParameter("q", query)
+                .build();
+        Intent marketSearchIntent = new Intent(Intent.ACTION_VIEW);
+        marketSearchIntent.setData(marketSearchUri);
+        return marketSearchIntent;
+    }
 }
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 730c8d1..5aa973a 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -90,6 +90,8 @@
         RecyclerView.RecycledViewPool pool = getRecycledViewPool();
         int approxRows = (int) Math.ceil(grid.availableHeightPx / grid.allAppsIconSizePx);
         pool.setMaxRecycledViews(AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE, 1);
+        pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE, 1);
+        pool.setMaxRecycledViews(AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE, 1);
         pool.setMaxRecycledViews(AllAppsGridAdapter.ICON_VIEW_TYPE, approxRows * mNumAppsPerRow);
         pool.setMaxRecycledViews(AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE, mNumAppsPerRow);
         pool.setMaxRecycledViews(AllAppsGridAdapter.SECTION_BREAK_VIEW_TYPE, approxRows);
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 47241ce..396f757 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -81,12 +81,12 @@
         public int position;
         // The type of this item
         public int viewType;
-        // The row that this item shows up on
-        public int rowIndex;
 
         /** Section & App properties */
         // The section for this item
         public SectionInfo sectionInfo;
+        // The row that this item shows up on
+        public int rowIndex;
 
         /** App-only properties */
         // The section name of this app.  Note that there can be multiple items with different
@@ -111,14 +111,14 @@
         }
 
         public static AdapterItem asPredictedApp(int pos, SectionInfo section, String sectionName,
-                                        int sectionAppIndex, AppInfo appInfo, int appIndex) {
+                int sectionAppIndex, AppInfo appInfo, int appIndex) {
             AdapterItem item = asApp(pos, section, sectionName, sectionAppIndex, appInfo, appIndex);
             item.viewType = AllAppsGridAdapter.PREDICTION_ICON_VIEW_TYPE;
             return item;
         }
 
         public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
-                                        int sectionAppIndex, AppInfo appInfo, int appIndex) {
+                int sectionAppIndex, AppInfo appInfo, int appIndex) {
             AdapterItem item = new AdapterItem();
             item.viewType = AllAppsGridAdapter.ICON_VIEW_TYPE;
             item.position = pos;
@@ -129,6 +129,27 @@
             item.appIndex = appIndex;
             return item;
         }
+
+        public static AdapterItem asEmptySearch(int pos) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.EMPTY_SEARCH_VIEW_TYPE;
+            item.position = pos;
+            return item;
+        }
+
+        public static AdapterItem asDivider(int pos) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.SEARCH_MARKET_DIVIDER_VIEW_TYPE;
+            item.position = pos;
+            return item;
+        }
+
+        public static AdapterItem asMarketSearch(int pos) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE;
+            item.position = pos;
+            return item;
+        }
     }
 
     /**
@@ -167,6 +188,7 @@
     private int mNumAppsPerRow;
     private int mNumPredictedAppsPerRow;
     private int mNumAppRowsInAdapter;
+    private boolean mDisableEmptyText;
 
     public AlphabeticalAppsList(Context context) {
         mLauncher = (Launcher) context;
@@ -194,6 +216,13 @@
     }
 
     /**
+     * Disables the empty text message when there are no search results.
+     */
+    public void disableEmptyText() {
+        mDisableEmptyText = true;
+    }
+
+    /**
      * Returns all the apps.
      */
     public List<AppInfo> getApps() {
@@ -222,13 +251,6 @@
     }
 
     /**
-     * Returns the number of applications in this list.
-     */
-    public int getSize() {
-        return mFilteredApps.size();
-    }
-
-    /**
      * Returns the number of rows of applications (not including predictions)
      */
     public int getNumAppRows() {
@@ -236,6 +258,13 @@
     }
 
     /**
+     * Returns the number of applications in this list.
+     */
+    public int getNumFilteredApps() {
+        return mFilteredApps.size();
+    }
+
+    /**
      * Returns whether there are is a filter set.
      */
     public boolean hasFilter() {
@@ -457,6 +486,16 @@
             mFilteredApps.add(info);
         }
 
+        // Append the search market item if we are currently searching
+        if (hasFilter()) {
+            if (hasNoFilteredResults()) {
+                mAdapterItems.add(AdapterItem.asEmptySearch(position++));
+            } else {
+                mAdapterItems.add(AdapterItem.asDivider(position++));
+            }
+            mAdapterItems.add(AdapterItem.asMarketSearch(position++));
+        }
+
         // Merge multiple sections together as requested by the merge strategy for this device
         mergeSections();
 
diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java
index 83b9205..14cbb95 100644
--- a/src/com/android/launcher3/allapps/DefaultAppSearchController.java
+++ b/src/com/android/launcher3/allapps/DefaultAppSearchController.java
@@ -169,19 +169,21 @@
         if (actionId != EditorInfo.IME_ACTION_DONE) {
             return false;
         }
-        // Skip if there isn't exactly one item
-        if (mApps.getSize() != 1) {
+        // Skip if there are more than one icon
+        if (mApps.getNumFilteredApps() > 1) {
             return false;
         }
-        // If there is exactly one icon, then quick-launch it
+        // Otherwise, find the first icon, or fallback to the search-market-view and launch it
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
         for (int i = 0; i < items.size(); i++) {
             AlphabeticalAppsList.AdapterItem item = items.get(i);
-            if (item.viewType == AllAppsGridAdapter.ICON_VIEW_TYPE) {
-                mAppsRecyclerView.getChildAt(i).performClick();
-                mInputMethodManager.hideSoftInputFromWindow(
-                        mContainerView.getWindowToken(), 0);
-                return true;
+            switch (item.viewType) {
+                case AllAppsGridAdapter.ICON_VIEW_TYPE:
+                case AllAppsGridAdapter.SEARCH_MARKET_VIEW_TYPE:
+                    mAppsRecyclerView.getChildAt(i).performClick();
+                    mInputMethodManager.hideSoftInputFromWindow(
+                            mContainerView.getWindowToken(), 0);
+                    return true;
             }
         }
         return false;
diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java
index 34492e4..8702877 100644
--- a/src/com/android/launcher3/testing/LauncherExtension.java
+++ b/src/com/android/launcher3/testing/LauncherExtension.java
@@ -202,6 +202,11 @@
         }
 
         @Override
+        public boolean startSearchFromAllApps(String query) {
+            return false;
+        }
+
+        @Override
         public void startVoice() {
         }