Moving apps search related logic into a custom layout file

This will allow derivative projects to easily change the search behavior
by simply overriding the xml file

Bug: 37616877
Change-Id: Ib8d6a2dab06819a52611e9a3d97c70c5a49bbf97
diff --git a/res/layout/all_apps.xml b/res/layout/all_apps.xml
index d6bdac2..7d97f25 100644
--- a/res/layout/all_apps.xml
+++ b/res/layout/all_apps.xml
@@ -64,33 +64,9 @@
             android:layout_alignParentEnd="true"
             android:layout_marginEnd="@dimen/container_fastscroll_popup_margin" />
 
-        <FrameLayout
-            android:id="@+id/search_container"
-            android:layout_width="match_parent"
-            android:layout_height="@dimen/all_apps_search_bar_height"
-            android:layout_gravity="center|top"
-            android:gravity="center|bottom"
-            android:orientation="horizontal"
-            android:saveEnabled="false">
-
-            <com.android.launcher3.ExtendedEditText
-                android:id="@+id/search_box_input"
-                android:layout_width="match_parent"
-                android:layout_height="@dimen/all_apps_search_bar_field_height"
-                android:background="@android:color/transparent"
-                android:layout_gravity="bottom"
-                android:focusableInTouchMode="true"
-                android:gravity="center"
-                android:imeOptions="actionSearch|flagNoExtractUi"
-                android:inputType="text|textNoSuggestions|textCapWords"
-                android:maxLines="1"
-                android:scrollHorizontally="true"
-                android:singleLine="true"
-                android:textColor="?android:attr/textColorSecondary"
-                android:hint="@string/all_apps_search_bar_hint"
-                android:textColorHint="@drawable/all_apps_search_hint"
-                android:textSize="16sp" />
-        </FrameLayout>
+        <include
+            layout="@layout/all_apps_search_container"
+            android:id="@+id/search_container" />
 
     </com.android.launcher3.allapps.AllAppsRecyclerViewContainerView>
     <View
diff --git a/res/layout/all_apps_search_container.xml b/res/layout/all_apps_search_container.xml
new file mode 100644
index 0000000..6addee1
--- /dev/null
+++ b/res/layout/all_apps_search_container.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.launcher3.allapps.search.AppsSearchContainerLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/all_apps_search_bar_height"
+    android:layout_gravity="center|top"
+    android:gravity="center|bottom"
+    android:id="@+id/search_container"
+    android:saveEnabled="false">
+
+    <com.android.launcher3.ExtendedEditText
+        android:id="@+id/search_box_input"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/all_apps_search_bar_field_height"
+        android:background="@android:color/transparent"
+        android:layout_gravity="bottom"
+        android:focusableInTouchMode="true"
+        android:gravity="center"
+        android:imeOptions="actionSearch|flagNoExtractUi"
+        android:inputType="text|textNoSuggestions|textCapWords"
+        android:maxLines="1"
+        android:scrollHorizontally="true"
+        android:singleLine="true"
+        android:textColor="?android:attr/textColorSecondary"
+        android:hint="@string/all_apps_search_bar_hint"
+        android:textColorHint="@drawable/all_apps_search_hint"
+        android:textSize="16sp" />
+</com.android.launcher3.allapps.search.AppsSearchContainerLayout>
\ No newline at end of file
diff --git a/res/layout/widgets_view.xml b/res/layout/widgets_view.xml
index 2f11c28..476901d 100644
--- a/res/layout/widgets_view.xml
+++ b/res/layout/widgets_view.xml
@@ -51,7 +51,6 @@
         <!-- Fast scroller popup -->
         <TextView
             style="@style/FastScrollerPopup"
-            android:layout_below="@+id/search_container"
             android:id="@+id/fast_scroller_popup"
             android:layout_gravity="top|end"
             android:layout_marginEnd="@dimen/container_fastscroll_popup_margin" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 751954f..71984d6 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -13,6 +13,10 @@
      easily override the app name without providing all translations -->
     <string name="derived_app_name" translatable="false">@string/app_name</string>
 
+    <!-- String representing the intent for search on the apps market. To specify a query, add
+    q=<query> to the data to the intent -->
+    <string name="market_search_intent" translatable="false">market://search?c=apps</string>
+
     <!-- Values for icon shape overrides. These should correspond to entries defined
      in icon_shape_override_paths_names -->
     <string-array name="icon_shape_override_paths_values">
diff --git a/src/com/android/launcher3/BaseRecyclerView.java b/src/com/android/launcher3/BaseRecyclerView.java
index 6fdf454..c056336 100644
--- a/src/com/android/launcher3/BaseRecyclerView.java
+++ b/src/com/android/launcher3/BaseRecyclerView.java
@@ -82,10 +82,6 @@
         }
     }
 
-    public void reset() {
-        mScrollbar.reattachThumbToScroll();
-    }
-
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index d0c01f4..c96c2a7 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -85,7 +85,6 @@
 import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
 import com.android.launcher3.allapps.AllAppsContainerView;
 import com.android.launcher3.allapps.AllAppsTransitionController;
-import com.android.launcher3.allapps.DefaultAppSearchController;
 import com.android.launcher3.anim.AnimationLayerSet;
 import com.android.launcher3.compat.AppWidgetManagerCompat;
 import com.android.launcher3.compat.LauncherAppsCompat;
@@ -560,47 +559,6 @@
 
     public boolean setLauncherCallbacks(LauncherCallbacks callbacks) {
         mLauncherCallbacks = callbacks;
-        mLauncherCallbacks.setLauncherSearchCallback(new Launcher.LauncherSearchCallbacks() {
-            private boolean mWorkspaceImportanceStored = false;
-            private boolean mHotseatImportanceStored = false;
-            private int mWorkspaceImportanceForAccessibility =
-                    View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
-            private int mHotseatImportanceForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
-
-            @Override
-            public void onSearchOverlayOpened() {
-                if (mWorkspaceImportanceStored || mHotseatImportanceStored) {
-                    return;
-                }
-                // The underlying workspace and hotseat are temporarily suppressed by the search
-                // overlay. So they shouldn't be accessible.
-                if (mWorkspace != null) {
-                    mWorkspaceImportanceForAccessibility =
-                            mWorkspace.getImportantForAccessibility();
-                    mWorkspace.setImportantForAccessibility(
-                            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-                    mWorkspaceImportanceStored = true;
-                }
-                if (mHotseat != null) {
-                    mHotseatImportanceForAccessibility = mHotseat.getImportantForAccessibility();
-                    mHotseat.setImportantForAccessibility(
-                            View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
-                    mHotseatImportanceStored = true;
-                }
-            }
-
-            @Override
-            public void onSearchOverlayClosed() {
-                if (mWorkspaceImportanceStored && mWorkspace != null) {
-                    mWorkspace.setImportantForAccessibility(mWorkspaceImportanceForAccessibility);
-                }
-                if (mHotseatImportanceStored && mHotseat != null) {
-                    mHotseat.setImportantForAccessibility(mHotseatImportanceForAccessibility);
-                }
-                mWorkspaceImportanceStored = false;
-                mHotseatImportanceStored = false;
-            }
-        });
         return true;
     }
 
@@ -1140,18 +1098,6 @@
         public void setOverlayCallbacks(LauncherOverlayCallbacks callbacks);
     }
 
-    public interface LauncherSearchCallbacks {
-        /**
-         * Called when the search overlay is shown.
-         */
-        public void onSearchOverlayOpened();
-
-        /**
-         * Called when the search overlay is dismissed.
-         */
-        public void onSearchOverlayClosed();
-    }
-
     public interface LauncherOverlayCallbacks {
         public void onScrollChanged(float progress);
     }
@@ -1344,11 +1290,6 @@
         // Setup Apps and Widgets
         mAppsView = (AllAppsContainerView) findViewById(R.id.apps_view);
         mWidgetsView = (WidgetsContainerView) findViewById(R.id.widgets_view);
-        if (mLauncherCallbacks != null && mLauncherCallbacks.getAllAppsSearchBarController() != null) {
-            mAppsView.setSearchBarController(mLauncherCallbacks.getAllAppsSearchBarController());
-        } else {
-            mAppsView.setSearchBarController(new DefaultAppSearchController());
-        }
 
         // Setup the drag controller (drop targets have to be added in reverse order in priority)
         mDragController.setMoveTarget(mWorkspace);
@@ -1777,7 +1718,7 @@
 
             // Reset the apps view
             if (!alreadyOnHome && mAppsView != null) {
-                mAppsView.scrollToTop();
+                mAppsView.reset();
             }
 
             // Reset the widgets view
diff --git a/src/com/android/launcher3/LauncherCallbacks.java b/src/com/android/launcher3/LauncherCallbacks.java
index 32f179f..ea4aeb9 100644
--- a/src/com/android/launcher3/LauncherCallbacks.java
+++ b/src/com/android/launcher3/LauncherCallbacks.java
@@ -21,7 +21,6 @@
 import android.view.Menu;
 import android.view.View;
 
-import com.android.launcher3.allapps.AllAppsSearchBarController;
 import com.android.launcher3.util.ComponentKey;
 
 import java.io.FileDescriptor;
@@ -92,20 +91,11 @@
      */
     boolean shouldMoveToDefaultScreenOnHomeIntent();
     boolean hasSettings();
-    AllAppsSearchBarController getAllAppsSearchBarController();
     List<ComponentKey> getPredictedApps();
     int SEARCH_BAR_HEIGHT_NORMAL = 0, SEARCH_BAR_HEIGHT_TALL = 1;
     /** Must return one of {@link #SEARCH_BAR_HEIGHT_NORMAL} or {@link #SEARCH_BAR_HEIGHT_TALL} */
     int getSearchBarHeight();
 
-    /**
-     * Sets the callbacks to allow reacting the actions of search overlays of the launcher.
-     *
-     * @param callbacks A set of callbacks to the Launcher, is actually a LauncherSearchCallback,
-     *                  but for implementation purposes is passed around as an object.
-     */
-    void setLauncherSearchCallback(Object callbacks);
-
     boolean shouldShowDiscoveryBounce();
 
     void onExtractedColorsChanged();
diff --git a/src/com/android/launcher3/allapps/AllAppsContainerView.java b/src/com/android/launcher3/allapps/AllAppsContainerView.java
index 7be8e8f..0ea61f4 100644
--- a/src/com/android/launcher3/allapps/AllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsContainerView.java
@@ -20,15 +20,9 @@
 import android.graphics.Rect;
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.InsetDrawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.text.Selection;
-import android.text.Spannable;
-import android.text.SpannableString;
 import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
-import android.text.method.TextKeyListener;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -42,7 +36,6 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.DragSource;
 import com.android.launcher3.DropTarget;
-import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Insettable;
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.Launcher;
@@ -50,18 +43,14 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.discovery.AppDiscoveryItem;
-import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.folder.Folder;
-import com.android.launcher3.graphics.TintedDrawableSpan;
 import com.android.launcher3.keyboard.FocusedItemDecorator;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Target;
 import com.android.launcher3.util.ComponentKey;
 import com.android.launcher3.util.PackageUserKey;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -69,7 +58,7 @@
  * The all apps view container.
  */
 public class AllAppsContainerView extends BaseContainerView implements DragSource,
-        View.OnLongClickListener, AllAppsSearchBarController.Callbacks, Insettable {
+        View.OnLongClickListener, Insettable {
 
     private final Launcher mLauncher;
     private final AlphabeticalAppsList mApps;
@@ -77,12 +66,8 @@
     private final RecyclerView.LayoutManager mLayoutManager;
 
     private AllAppsRecyclerView mAppsRecyclerView;
-    private AllAppsSearchBarController mSearchBarController;
-
+    private SearchUiManager mSearchUiManager;
     private View mSearchContainer;
-    private int mSearchContainerMinHeight;
-    private ExtendedEditText mSearchInput;
-    private HeaderElevationController mElevationController;
 
     private SpannableStringBuilder mSearchQueryBuilder = null;
 
@@ -106,8 +91,6 @@
         mApps.setAdapter(mAdapter);
         mLayoutManager = mAdapter.getLayoutManager();
         mSearchQueryBuilder = new SpannableStringBuilder();
-        mSearchContainerMinHeight
-                = getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_height);
 
         Selection.setSelection(mSearchQueryBuilder, 0);
     }
@@ -149,7 +132,7 @@
      */
     public void addApps(List<AppInfo> apps) {
         mApps.addApps(apps);
-        mSearchBarController.refreshSearchResult();
+        mSearchUiManager.refreshSearchResult();
     }
 
     /**
@@ -157,7 +140,7 @@
      */
     public void updateApps(List<AppInfo> apps) {
         mApps.updateApps(apps);
-        mSearchBarController.refreshSearchResult();
+        mSearchUiManager.refreshSearchResult();
     }
 
     public void updatePromiseAppProgress(PromiseAppInfo app) {
@@ -176,34 +159,7 @@
      */
     public void removeApps(List<AppInfo> apps) {
         mApps.removeApps(apps);
-        mSearchBarController.refreshSearchResult();
-    }
-
-    public void setSearchBarVisible(boolean visible) {
-        if (visible) {
-            mSearchBarController.setVisibility(View.VISIBLE);
-        } else {
-            mSearchBarController.setVisibility(View.INVISIBLE);
-        }
-    }
-
-    /**
-     * Sets the search bar that shows above the a-z list.
-     */
-    public void setSearchBarController(AllAppsSearchBarController searchController) {
-        if (mSearchBarController != null) {
-            throw new RuntimeException("Expected search bar controller to only be set once");
-        }
-        mSearchBarController = searchController;
-        mSearchBarController.initialize(mApps, mSearchInput, mLauncher, this);
-        mAdapter.setSearchController(mSearchBarController);
-    }
-
-    /**
-     * Scrolls this list view to the top.
-     */
-    public void scrollToTop() {
-        mAppsRecyclerView.scrollToTop();
+        mSearchUiManager.refreshSearchResult();
     }
 
     /**
@@ -238,9 +194,7 @@
      * Focuses the search field and begins an app search.
      */
     public void startAppsSearch() {
-        if (mSearchBarController != null) {
-            mSearchBarController.focusSearchField();
-        }
+        mSearchUiManager.startAppsSearch();
     }
 
     /**
@@ -248,9 +202,8 @@
      */
     public void reset() {
         // Reset the search bar and base recycler view after transitioning home
-        scrollToTop();
-        mSearchBarController.reset();
-        mAppsRecyclerView.reset();
+        mAppsRecyclerView.scrollToTop();
+        mSearchUiManager.reset();
     }
 
     @Override
@@ -268,28 +221,17 @@
             }
         });
 
-        mSearchContainer = findViewById(R.id.search_container);
-        mSearchInput = (ExtendedEditText) findViewById(R.id.search_box_input);
-
-        // Update the hint to contain the icon.
-        // Prefix the original hint with two spaces. The first space gets replaced by the icon
-        // using span. The second space is used for a singe space character between the hint
-        // and the icon.
-        SpannableString spanned = new SpannableString("  " + mSearchInput.getHint());
-        spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search),
-                0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
-        mSearchInput.setHint(spanned);
-
-        mElevationController = new HeaderElevationController(mSearchContainer);
-
         // Load the all apps recycler view
         mAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);
         mAppsRecyclerView.setApps(mApps);
         mAppsRecyclerView.setLayoutManager(mLayoutManager);
         mAppsRecyclerView.setAdapter(mAdapter);
         mAppsRecyclerView.setHasFixedSize(true);
-        mAppsRecyclerView.addOnScrollListener(mElevationController);
-        mAppsRecyclerView.setElevationController(mElevationController);
+
+        mSearchContainer = findViewById(R.id.search_container);
+        mSearchUiManager = (SearchUiManager) mSearchContainer;
+        mSearchUiManager.initialize(mApps, mAppsRecyclerView);
+
 
         FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView);
         mAppsRecyclerView.addItemDecoration(focusedItemDecorator);
@@ -309,12 +251,11 @@
     }
 
     @Override
-    public void onBoundsChanged(Rect newBounds) { }
-
-    @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         DeviceProfile grid = mLauncher.getDeviceProfile();
+        // Update the number of items in the grid before we measure the view
         grid.updateAppsViewNumCols();
+
         if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) {
             if (mNumAppsPerRow != grid.inv.numColumns ||
                     mNumPredictedAppsPerRow != grid.inv.numColumns) {
@@ -325,22 +266,11 @@
                 mAdapter.setNumAppsPerRow(mNumAppsPerRow);
                 mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
             }
-            if (!grid.isVerticalBarLayout()) {
-                MarginLayoutParams searchContainerLp =
-                        (MarginLayoutParams) mSearchContainer.getLayoutParams();
-
-                searchContainerLp.height = mLauncher.getDragLayer().getInsets().top
-                        + mSearchContainerMinHeight;
-                mSearchContainer.setLayoutParams(searchContainerLp);
-            }
             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
             return;
         }
 
         // --- remove START when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. ---
-
-        // Update the number of items in the grid before we measure the view
-        grid.updateAppsViewNumCols();
         if (mNumAppsPerRow != grid.allAppsNumCols ||
                 mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) {
             mNumAppsPerRow = grid.allAppsNumCols;
@@ -357,22 +287,7 @@
 
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
-        // Determine if the key event was actual text, if so, focus the search bar and then dispatch
-        // the key normally so that it can process this key event
-        if (!mSearchBarController.isSearchFieldFocused() &&
-                event.getAction() == KeyEvent.ACTION_DOWN) {
-            final int unicodeChar = event.getUnicodeChar();
-            final boolean isKeyNotWhitespace = unicodeChar > 0 &&
-                    !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar);
-            if (isKeyNotWhitespace) {
-                boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder,
-                        event.getKeyCode(), event);
-                if (gotKey && mSearchQueryBuilder.length() > 0) {
-                    mSearchBarController.focusSearchField();
-                }
-            }
-        }
-
+        mSearchUiManager.preDispatchKeyEvent(event);
         return super.dispatchKeyEvent(event);
     }
 
@@ -440,42 +355,12 @@
     }
 
     @Override
-    public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
-        if (apps != null) {
-            mApps.setOrderedFilter(apps);
-            mAppsRecyclerView.onSearchResultsChanged();
-            mAdapter.setLastSearchQuery(query);
-        }
-    }
-
-    @Override
-    public void onAppDiscoverySearchUpdate(@Nullable AppDiscoveryItem app,
-            @NonNull AppDiscoveryUpdateState state) {
-        if (!mLauncher.isDestroyed()) {
-            mApps.onAppDiscoverySearchUpdate(app, state);
-            mAppsRecyclerView.onSearchResultsChanged();
-        }
-    }
-
-    @Override
-    public void clearSearchResult() {
-        if (mApps.setOrderedFilter(null)) {
-            mAppsRecyclerView.onSearchResultsChanged();
-        }
-
-        // Clear the search query
-        mSearchQueryBuilder.clear();
-        mSearchQueryBuilder.clearSpans();
-        Selection.setSelection(mSearchQueryBuilder, 0);
-    }
-
-    @Override
     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
         targetParent.containerType = mAppsRecyclerView.getContainerType(v);
     }
 
     public boolean shouldRestoreImeState() {
-        return !TextUtils.isEmpty(mSearchInput.getText());
+        return mSearchUiManager.shouldRestoreImeState();
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
index 938e84e..e126102 100644
--- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
+++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java
@@ -40,6 +40,7 @@
 import com.android.launcher3.allapps.AlphabeticalAppsList.AdapterItem;
 import com.android.launcher3.discovery.AppDiscoveryAppInfo;
 import com.android.launcher3.discovery.AppDiscoveryItemView;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.List;
 
@@ -199,7 +200,6 @@
     private int mAppsPerRow;
 
     private BindViewCallback mBindViewCallback;
-    private AllAppsSearchBarController mSearchController;
     private OnFocusChangeListener mIconFocusListener;
 
     // The text to show when there are no search results and no market search handler.
@@ -241,10 +241,6 @@
         mGridLayoutMgr.setSpanCount(appsPerRow);
     }
 
-    public void setSearchController(AllAppsSearchBarController searchController) {
-        mSearchController = searchController;
-    }
-
     public void setIconFocusListener(OnFocusChangeListener focusListener) {
         mIconFocusListener = focusListener;
     }
@@ -256,7 +252,7 @@
     public void setLastSearchQuery(String query) {
         Resources res = mLauncher.getResources();
         mEmptySearchMessage = res.getString(R.string.all_apps_no_search_results, query);
-        mMarketSearchIntent = mSearchController.createMarketSearchIntent(query);
+        mMarketSearchIntent = PackageManagerHelper.getMarketSearchIntent(mLauncher, query);
     }
 
     /**
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index 64e2fcb..f3089d2 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -53,8 +53,6 @@
     private AllAppsBackgroundDrawable mEmptySearchBackground;
     private int mEmptySearchBackgroundTopOffset;
 
-    private HeaderElevationController mElevationController;
-
     public AllAppsRecyclerView(Context context) {
         this(context, null);
     }
@@ -85,10 +83,6 @@
         mFastScrollHelper = new AllAppsFastScrollHelper(this, apps);
     }
 
-    public void setElevationController(HeaderElevationController elevationController) {
-        mElevationController = elevationController;
-    }
-
     /**
      * Sets the number of apps per row in this recycler view.
      */
@@ -152,13 +146,8 @@
      */
     public void scrollToTop() {
         // Ensure we reattach the scrollbar if it was previously detached while fast-scrolling
-        if (mScrollbar.isThumbDetached()) {
-            mScrollbar.reattachThumbToScroll();
-        }
+        mScrollbar.reattachThumbToScroll();
         scrollToPosition(0);
-        if (mElevationController != null) {
-            mElevationController.reset();
-        }
     }
 
     @Override
diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchController.java b/src/com/android/launcher3/allapps/DefaultAppSearchController.java
deleted file mode 100644
index 57747e3..0000000
--- a/src/com/android/launcher3/allapps/DefaultAppSearchController.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright (C) 2015 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.launcher3.allapps;
-
-/**
- * The default search controller.
- */
-public class DefaultAppSearchController extends AllAppsSearchBarController {
-
-    public DefaultAppSearchAlgorithm onInitializeSearch() {
-        return new DefaultAppSearchAlgorithm(mApps.getApps());
-    }
-}
diff --git a/src/com/android/launcher3/allapps/SearchUiManager.java b/src/com/android/launcher3/allapps/SearchUiManager.java
new file mode 100644
index 0000000..15455bc
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SearchUiManager.java
@@ -0,0 +1,58 @@
+/*
+ * 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.launcher3.allapps;
+
+import android.view.KeyEvent;
+
+/**
+ * Interface for controlling the Apps search UI.
+ */
+public interface SearchUiManager {
+
+    /**
+     * Initializes the search manager.
+     */
+    void initialize(AlphabeticalAppsList appsList, AllAppsRecyclerView recyclerView);
+
+    /**
+     * Notifies the search manager that the apps-list has changed and the search UI should be
+     * updated accordingly.
+     */
+    void refreshSearchResult();
+
+    /**
+     * Notifies the search manager to close any active search session.
+     */
+    void reset();
+
+    /**
+     * Called before dispatching a key event, in case the search manager wants to initialize
+     * some UI beforehand.
+     */
+    void preDispatchKeyEvent(KeyEvent keyEvent);
+
+    /**
+     * Returns true if the IME should be brought back.
+     * TODO: Remove when removing support for opening all-apps in search mode.
+     */
+    boolean shouldRestoreImeState();
+
+    /**
+     * Starts the search UI
+     * TODO: Remove when removing support for opening all-apps in search mode.
+     */
+    void startAppsSearch();
+}
diff --git a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
similarity index 85%
rename from src/com/android/launcher3/allapps/AllAppsSearchBarController.java
rename to src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
index c7ba3ab..547d9e1 100644
--- a/src/com/android/launcher3/allapps/AllAppsSearchBarController.java
+++ b/src/com/android/launcher3/allapps/search/AllAppsSearchBarController.java
@@ -13,12 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.allapps;
+package com.android.launcher3.allapps.search;
 
 import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.net.Uri;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.text.Editable;
@@ -34,16 +31,18 @@
 import com.android.launcher3.ExtendedEditText;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.allapps.AlphabeticalAppsList;
 import com.android.launcher3.discovery.AppDiscoveryItem;
 import com.android.launcher3.discovery.AppDiscoveryUpdateState;
 import com.android.launcher3.util.ComponentKey;
+import com.android.launcher3.util.PackageManagerHelper;
 
 import java.util.ArrayList;
 
 /**
  * An interface to a search box that AllApps can command.
  */
-public abstract class AllAppsSearchBarController
+public class AllAppsSearchBarController
         implements TextWatcher, OnEditorActionListener, ExtendedEditText.OnBackKeyListener {
 
     protected Launcher mLauncher;
@@ -88,9 +87,11 @@
     }
 
     /**
-     * To be implemented by subclasses. This method will get called when the controller is set.
+     * This method will get called when the controller is set.
      */
-    protected abstract DefaultAppSearchAlgorithm onInitializeSearch();
+    public DefaultAppSearchAlgorithm onInitializeSearch() {
+        return new DefaultAppSearchAlgorithm(mApps.getApps());
+    }
 
     @Override
     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
@@ -114,7 +115,7 @@
         }
     }
 
-    protected void refreshSearchResult() {
+    public void refreshSearchResult() {
         if (TextUtils.isEmpty(mQuery)) {
             return;
         }
@@ -135,7 +136,8 @@
         if (query.isEmpty()) {
             return false;
         }
-        return mLauncher.startActivitySafely(v, createMarketSearchIntent(query), null);
+        return mLauncher.startActivitySafely(v,
+                PackageManagerHelper.getMarketSearchIntent(mLauncher, query), null);
     }
 
     @Override
@@ -186,29 +188,11 @@
     }
 
     /**
-     * Creates a new market search intent.
-     */
-    public Intent createMarketSearchIntent(String query) {
-        Uri marketSearchUri = Uri.parse("market://search")
-                .buildUpon()
-                .appendQueryParameter("c", "apps")
-                .appendQueryParameter("q", query)
-                .build();
-        return new Intent(Intent.ACTION_VIEW).setData(marketSearchUri);
-    }
-
-    /**
      * Callback for getting search results.
      */
     public interface Callbacks {
 
         /**
-         * Called when the bounds of the search bar has changed.
-         */
-        @Deprecated
-        void onBoundsChanged(Rect newBounds);
-
-        /**
          * Called when the search is complete.
          *
          * @param apps sorted list of matching components or null if in case of failure.
@@ -220,7 +204,6 @@
          */
         void clearSearchResult();
 
-
         /**
          * Called when the app discovery is providing an update of search, which can either be
          * START for starting a new discovery,
diff --git a/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
new file mode 100644
index 0000000..116ec88
--- /dev/null
+++ b/src/com/android/launcher3/allapps/search/AppsSearchContainerLayout.java
@@ -0,0 +1,195 @@
+/*
+ * 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.launcher3.allapps.search;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.method.TextKeyListener;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.FrameLayout;
+
+import com.android.launcher3.ExtendedEditText;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.allapps.AllAppsGridAdapter;
+import com.android.launcher3.allapps.AllAppsRecyclerView;
+import com.android.launcher3.allapps.AlphabeticalAppsList;
+import com.android.launcher3.allapps.SearchUiManager;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.launcher3.discovery.AppDiscoveryItem;
+import com.android.launcher3.discovery.AppDiscoveryUpdateState;
+import com.android.launcher3.graphics.TintedDrawableSpan;
+import com.android.launcher3.util.ComponentKey;
+
+import java.util.ArrayList;
+
+/**
+ * Layout to contain the All-apps search UI.
+ */
+public class AppsSearchContainerLayout extends FrameLayout
+        implements SearchUiManager, AllAppsSearchBarController.Callbacks {
+
+    private final Launcher mLauncher;
+    private final int mMinHeight;
+    private final AllAppsSearchBarController mSearchBarController;
+    private final SpannableStringBuilder mSearchQueryBuilder;
+    private final HeaderElevationController mElevationController;
+
+    private ExtendedEditText mSearchInput;
+    private AlphabeticalAppsList mApps;
+    private AllAppsRecyclerView mAppsRecyclerView;
+    private AllAppsGridAdapter mAdapter;
+
+    public AppsSearchContainerLayout(Context context) {
+        this(context, null);
+    }
+
+    public AppsSearchContainerLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AppsSearchContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        mLauncher = Launcher.getLauncher(context);
+        mMinHeight = getResources().getDimensionPixelSize(R.dimen.all_apps_search_bar_height);
+        mSearchBarController = new AllAppsSearchBarController();
+        mElevationController = new HeaderElevationController(this);
+
+        mSearchQueryBuilder = new SpannableStringBuilder();
+        Selection.setSelection(mSearchQueryBuilder, 0);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mSearchInput = findViewById(R.id.search_box_input);
+
+        // Update the hint to contain the icon.
+        // Prefix the original hint with two spaces. The first space gets replaced by the icon
+        // using span. The second space is used for a singe space character between the hint
+        // and the icon.
+        SpannableString spanned = new SpannableString("  " + mSearchInput.getHint());
+        spanned.setSpan(new TintedDrawableSpan(getContext(), R.drawable.ic_allapps_search),
+                0, 1, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
+        mSearchInput.setHint(spanned);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP &&
+                !mLauncher.getDeviceProfile().isVerticalBarLayout()) {
+            getLayoutParams().height = mLauncher.getDragLayer().getInsets().top + mMinHeight;
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+
+    @Override
+    public void initialize(
+            AlphabeticalAppsList appsList, AllAppsRecyclerView recyclerView) {
+        mApps = appsList;
+        mAppsRecyclerView = recyclerView;
+        mAppsRecyclerView.addOnScrollListener(mElevationController);
+        mAdapter = (AllAppsGridAdapter) mAppsRecyclerView.getAdapter();
+
+        mSearchBarController.initialize(appsList, mSearchInput, mLauncher, this);
+    }
+
+    @Override
+    public void refreshSearchResult() {
+        mSearchBarController.refreshSearchResult();
+    }
+
+    @Override
+    public void reset() {
+        mElevationController.reset();
+        mSearchBarController.reset();
+    }
+
+    @Override
+    public void preDispatchKeyEvent(KeyEvent event) {
+        // Determine if the key event was actual text, if so, focus the search bar and then dispatch
+        // the key normally so that it can process this key event
+        if (!mSearchBarController.isSearchFieldFocused() &&
+                event.getAction() == KeyEvent.ACTION_DOWN) {
+            final int unicodeChar = event.getUnicodeChar();
+            final boolean isKeyNotWhitespace = unicodeChar > 0 &&
+                    !Character.isWhitespace(unicodeChar) && !Character.isSpaceChar(unicodeChar);
+            if (isKeyNotWhitespace) {
+                boolean gotKey = TextKeyListener.getInstance().onKeyDown(this, mSearchQueryBuilder,
+                        event.getKeyCode(), event);
+                if (gotKey && mSearchQueryBuilder.length() > 0) {
+                    mSearchBarController.focusSearchField();
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean shouldRestoreImeState() {
+        return !TextUtils.isEmpty(mSearchInput.getText());
+    }
+
+    @Override
+    public void startAppsSearch() {
+        if (mApps != null) {
+            mSearchBarController.focusSearchField();
+        }
+    }
+
+    @Override
+    public void onSearchResult(String query, ArrayList<ComponentKey> apps) {
+        if (apps != null) {
+            mApps.setOrderedFilter(apps);
+            notifyResultChanged();
+            mAdapter.setLastSearchQuery(query);
+        }
+    }
+
+    @Override
+    public void clearSearchResult() {
+        if (mApps.setOrderedFilter(null)) {
+            notifyResultChanged();
+        }
+
+        // Clear the search query
+        mSearchQueryBuilder.clear();
+        mSearchQueryBuilder.clearSpans();
+        Selection.setSelection(mSearchQueryBuilder, 0);
+    }
+
+    @Override
+    public void onAppDiscoverySearchUpdate(
+            @Nullable AppDiscoveryItem app, @NonNull AppDiscoveryUpdateState state) {
+        if (!mLauncher.isDestroyed()) {
+            mApps.onAppDiscoverySearchUpdate(app, state);
+            notifyResultChanged();
+        }
+    }
+
+    private void notifyResultChanged() {
+        mElevationController.reset();
+        mAppsRecyclerView.onSearchResultsChanged();
+    }
+}
diff --git a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
similarity index 98%
rename from src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
rename to src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
index 06cf9aa..457b454 100644
--- a/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithm.java
+++ b/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithm.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.allapps;
+package com.android.launcher3.allapps.search;
 
 import android.os.Handler;
 
diff --git a/src/com/android/launcher3/allapps/HeaderElevationController.java b/src/com/android/launcher3/allapps/search/HeaderElevationController.java
similarity index 97%
rename from src/com/android/launcher3/allapps/HeaderElevationController.java
rename to src/com/android/launcher3/allapps/search/HeaderElevationController.java
index b167fed..ab4e88f 100644
--- a/src/com/android/launcher3/allapps/HeaderElevationController.java
+++ b/src/com/android/launcher3/allapps/search/HeaderElevationController.java
@@ -1,4 +1,4 @@
-package com.android.launcher3.allapps;
+package com.android.launcher3.allapps.search;
 
 import android.content.res.Resources;
 import android.graphics.Outline;
diff --git a/src/com/android/launcher3/testing/LauncherExtension.java b/src/com/android/launcher3/testing/LauncherExtension.java
index 031da20..36df22c 100644
--- a/src/com/android/launcher3/testing/LauncherExtension.java
+++ b/src/com/android/launcher3/testing/LauncherExtension.java
@@ -10,7 +10,6 @@
 import com.android.launcher3.AppInfo;
 import com.android.launcher3.Launcher;
 import com.android.launcher3.LauncherCallbacks;
-import com.android.launcher3.allapps.AllAppsSearchBarController;
 import com.android.launcher3.util.ComponentKey;
 
 import java.io.FileDescriptor;
@@ -198,11 +197,6 @@
         }
 
         @Override
-        public AllAppsSearchBarController getAllAppsSearchBarController() {
-            return null;
-        }
-
-        @Override
         public List<ComponentKey> getPredictedApps() {
             // To debug app predictions, enable AlphabeticalAppsList#DEBUG_PREDICTIONS
             return new ArrayList<>();
@@ -214,11 +208,6 @@
         }
 
         @Override
-        public void setLauncherSearchCallback(Object callbacks) {
-            // Do nothing
-        }
-
-        @Override
         public void onAttachedToWindow() {
         }
 
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index e12b2d4..13034dd 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -30,9 +30,11 @@
 import android.text.TextUtils;
 
 import com.android.launcher3.AppInfo;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.compat.LauncherAppsCompat;
 
+import java.net.URISyntaxException;
 import java.util.List;
 
 /**
@@ -149,4 +151,20 @@
                         .appendQueryParameter("id", packageName)
                         .build());
     }
+
+    /**
+     * Creates a new market search intent.
+     */
+    public static Intent getMarketSearchIntent(Context context, String query) {
+        try {
+            Intent intent = Intent.parseUri(context.getString(R.string.market_search_intent), 0);
+            if (!TextUtils.isEmpty(query)) {
+                intent.setData(
+                        intent.getData().buildUpon().appendQueryParameter("q", query).build());
+            }
+            return intent;
+        } catch (URISyntaxException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java
rename to tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
index 18570de..20b23b0 100644
--- a/tests/src/com/android/launcher3/allapps/DefaultAppSearchAlgorithmTest.java
+++ b/tests/src/com/android/launcher3/allapps/search/DefaultAppSearchAlgorithmTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.launcher3.allapps;
+package com.android.launcher3.allapps.search;
 
 import android.content.ComponentName;
 import android.test.InstrumentationTestCase;