Pulling out predictions into another row view.

Change-Id: Iba0d74457a1314cf0c00a88f9b07df049334e542
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
index dc75637..eff7b06 100644
--- a/src/com/android/launcher3/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/AlphabeticalAppsList.java
@@ -151,11 +151,13 @@
      * Info about a particular adapter item (can be either section or app)
      */
     public static class AdapterItem {
-        /** Section & App properties */
+        /** Common properties */
         // The index of this adapter item in the list
         public int position;
-        // Whether or not the item at this adapter position is a section or not
-        public boolean isSectionHeader;
+        // The type of this item
+        public int viewType;
+
+        /** Section & App properties */
         // The section for this item
         public SectionInfo sectionInfo;
 
@@ -169,30 +171,33 @@
         public AppInfo appInfo = null;
         // The index of this app not including sections
         public int appIndex = -1;
-        // Whether or not this is a predicted app
-        public boolean isPredictedApp;
 
         public static AdapterItem asSectionBreak(int pos, SectionInfo section) {
             AdapterItem item = new AdapterItem();
+            item.viewType = AppsGridAdapter.SECTION_BREAK_VIEW_TYPE;
             item.position = pos;
-            item.isSectionHeader = true;
             item.sectionInfo = section;
             section.sectionBreakItem = item;
             return item;
         }
 
-        public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
-                                        int sectionAppIndex, AppInfo appInfo, int appIndex,
-                                        boolean isPredictedApp) {
+        public static AdapterItem asPredictionBarSpacer(int pos) {
             AdapterItem item = new AdapterItem();
+            item.viewType = AppsGridAdapter.PREDICTION_BAR_SPACER_TYPE;
             item.position = pos;
-            item.isSectionHeader = false;
+            return item;
+        }
+
+        public static AdapterItem asApp(int pos, SectionInfo section, String sectionName,
+                                        int sectionAppIndex, AppInfo appInfo, int appIndex) {
+            AdapterItem item = new AdapterItem();
+            item.viewType = AppsGridAdapter.ICON_VIEW_TYPE;
+            item.position = pos;
             item.sectionInfo = section;
             item.sectionName = sectionName;
             item.sectionAppIndex = sectionAppIndex;
             item.appInfo = appInfo;
             item.appIndex = appIndex;
-            item.isPredictedApp = isPredictedApp;
             return item;
         }
     }
@@ -205,6 +210,13 @@
     }
 
     /**
+     * A callback to notify of changes to the filter.
+     */
+    public interface FilterChangedCallback {
+        void onFilterChanged();
+    }
+
+    /**
      * Common interface for different merging strategies.
      */
     private interface MergeAlgorithm {
@@ -260,28 +272,31 @@
     private List<AdapterItem> mSectionedFilteredApps = new ArrayList<>();
     private List<SectionInfo> mSections = new ArrayList<>();
     private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
-    private List<ComponentName> mPredictedApps = new ArrayList<>();
+    private List<ComponentName> mPredictedAppComponents = new ArrayList<>();
+    private List<AppInfo> mPredictedApps = new ArrayList<>();
     private HashMap<CharSequence, String> mCachedSectionNames = new HashMap<>();
     private RecyclerView.Adapter mAdapter;
     private Filter mFilter;
     private AlphabeticIndexCompat mIndexer;
     private AppNameComparator mAppNameComparator;
     private MergeAlgorithm mMergeAlgorithm;
+    private FilterChangedCallback mFilterChangedCallback;
     private int mNumAppsPerRow;
+    private int mNumPredictedAppsPerRow;
 
-    public AlphabeticalAppsList(Context context, int numAppsPerRow) {
+    public AlphabeticalAppsList(Context context, FilterChangedCallback cb, int numAppsPerRow,
+            int numPredictedAppsPerRow) {
         mContext = context;
         mIndexer = new AlphabeticIndexCompat(context);
         mAppNameComparator = new AppNameComparator(context);
-        setNumAppsPerRow(numAppsPerRow);
+        mFilterChangedCallback = cb;
+        setNumAppsPerRow(numAppsPerRow, numPredictedAppsPerRow);
     }
 
     /**
      * Sets the number of apps per row.  Used only for AppsContainerView.SECTIONED_GRID_COALESCED.
      */
-    public void setNumAppsPerRow(int numAppsPerRow) {
-        mNumAppsPerRow = numAppsPerRow;
-
+    public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
         // Update the merge algorithm
         DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
         if (grid.isPhone()) {
@@ -291,6 +306,9 @@
             mMergeAlgorithm = new TabletMergeAlgorithm();
         }
 
+        mNumAppsPerRow = numAppsPerRow;
+        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
+
         onAppsUpdated();
     }
 
@@ -351,6 +369,9 @@
             mFilter = f;
             onAppsUpdated();
             mAdapter.notifyDataSetChanged();
+            if (mFilterChangedCallback != null){
+                mFilterChangedCallback.onFilterChanged();
+            }
         }
     }
 
@@ -359,13 +380,20 @@
      * of applications, we should merge the results only in onAppsUpdated() which is idempotent.
      */
     public void setPredictedApps(List<ComponentName> apps) {
-        mPredictedApps.clear();
-        mPredictedApps.addAll(apps);
+        mPredictedAppComponents.clear();
+        mPredictedAppComponents.addAll(apps);
         onAppsUpdated();
         mAdapter.notifyDataSetChanged();
     }
 
     /**
+     * Returns the current set of predicted apps.
+     */
+    public List<AppInfo> getPredictedApps() {
+        return mPredictedApps;
+    }
+
+    /**
      * Sets the current set of apps.
      */
     public void setApps(List<AppInfo> apps) {
@@ -450,6 +478,42 @@
         // Sort the list of apps
         Collections.sort(mApps, mAppNameComparator.getAppInfoComparator());
 
+        // Prepare to update the list of sections, filtered apps, etc.
+        mFilteredApps.clear();
+        mSections.clear();
+        mSectionedFilteredApps.clear();
+        mFastScrollerSections.clear();
+        SectionInfo lastSectionInfo = null;
+        String lastSectionName = null;
+        FastScrollSectionInfo lastFastScrollerSectionInfo = null;
+        int position = 0;
+        int appIndex = 0;
+        List<AppInfo> allApps = new ArrayList<>();
+
+
+        // Process the predicted app components
+        mPredictedApps.clear();
+        if (mPredictedAppComponents != null && !mPredictedAppComponents.isEmpty() && !hasFilter()) {
+            for (ComponentName cn : mPredictedAppComponents) {
+                for (AppInfo info : mApps) {
+                    if (cn.equals(info.componentName)) {
+                        mPredictedApps.add(info);
+                        break;
+                    }
+                }
+                // Stop at the number of predicted apps
+                if (mPredictedApps.size() == mNumPredictedAppsPerRow) {
+                    break;
+                }
+            }
+
+            if (!mPredictedApps.isEmpty()) {
+                // Create a new spacer for the prediction bar
+                AdapterItem sectionItem = AdapterItem.asPredictionBarSpacer(position++);
+                mSectionedFilteredApps.add(sectionItem);
+            }
+        }
+
         // As a special case for some languages (currently only Simplified Chinese), we may need to
         // coalesce sections
         Locale curLocale = mContext.getResources().getConfiguration().locale;
@@ -475,6 +539,11 @@
                 }
                 sectionApps.add(info);
             }
+
+            // Add it to the list
+            for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
+                allApps.addAll(entry.getValue());
+            }
         } else {
             // Just compute the section headers for use below
             for (AppInfo info : mApps) {
@@ -485,44 +554,7 @@
                     mCachedSectionNames.put(info.title, sectionName);
                 }
             }
-        }
-
-        // Prepare to update the list of sections, filtered apps, etc.
-        mFilteredApps.clear();
-        mSections.clear();
-        mSectionedFilteredApps.clear();
-        mFastScrollerSections.clear();
-        SectionInfo lastSectionInfo = null;
-        String lastSectionName = null;
-        FastScrollSectionInfo lastFastScrollerSectionInfo = null;
-        int position = 0;
-        int appIndex = 0;
-        List<AppInfo> allApps = new ArrayList<>();
-
-        // Add the predicted apps to the combined list
-        int numPredictedApps = 0;
-        if (mPredictedApps != null && !mPredictedApps.isEmpty() && !hasFilter()) {
-            for (ComponentName cn : mPredictedApps) {
-                for (AppInfo info : mApps) {
-                    if (cn.equals(info.componentName)) {
-                        allApps.add(info);
-                        numPredictedApps++;
-                        break;
-                    }
-                }
-                // Stop at the number of predicted apps
-                if (numPredictedApps == mNumAppsPerRow) {
-                    break;
-                }
-            }
-        }
-
-        // Add all the other apps to the combined list
-        if (localeRequiresSectionSorting) {
-            for (Map.Entry<String, ArrayList<AppInfo>> entry : sectionMap.entrySet()) {
-                allApps.addAll(entry.getValue());
-            }
-        } else {
+            // Add it to the list
             allApps.addAll(mApps);
         }
 
@@ -530,10 +562,9 @@
         // ordered set of sections
         int numApps = allApps.size();
         for (int i = 0; i < numApps; i++) {
-            boolean isPredictedApp = i < numPredictedApps;
             AppInfo info = allApps.get(i);
             // The section name was computed above so this should be find
-            String sectionName = isPredictedApp ? "" : mCachedSectionNames.get(info.title);
+            String sectionName = mCachedSectionNames.get(info.title);
 
             // Check if we want to retain this app
             if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
@@ -541,8 +572,7 @@
             }
 
             // Create a new section if the section names do not match
-            if (lastSectionInfo == null ||
-                    (!isPredictedApp && !sectionName.equals(lastSectionName))) {
+            if (lastSectionInfo == null || !sectionName.equals(lastSectionName)) {
                 lastSectionName = sectionName;
                 lastSectionInfo = new SectionInfo();
                 lastFastScrollerSectionInfo = new FastScrollSectionInfo(sectionName,
@@ -559,7 +589,7 @@
 
             // Create an app item
             AdapterItem appItem = AdapterItem.asApp(position++, lastSectionInfo, sectionName,
-                    lastSectionInfo.numApps++, info, appIndex++, isPredictedApp);
+                    lastSectionInfo.numApps++, info, appIndex++);
             if (lastSectionInfo.firstAppItem == null) {
                 lastSectionInfo.firstAppItem = appItem;
                 lastFastScrollerSectionInfo.appItem = appItem;
@@ -568,6 +598,14 @@
             mFilteredApps.add(info);
         }
 
+        // Merge multiple sections together as requested by the merge strategy for this device
+        mergeSections();
+    }
+
+    /**
+     * Merges multiple sections to reduce visual raggedness.
+     */
+    private void mergeSections() {
         // Go through each section and try and merge some of the sections
         if (AppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) {
             int sectionAppCount = 0;
diff --git a/src/com/android/launcher3/AppsContainerRecyclerView.java b/src/com/android/launcher3/AppsContainerRecyclerView.java
index e918bc2..3952923 100644
--- a/src/com/android/launcher3/AppsContainerRecyclerView.java
+++ b/src/com/android/launcher3/AppsContainerRecyclerView.java
@@ -45,8 +45,6 @@
      * scroller.
      */
     private static class ScrollPositionState {
-        // The index of the first app in the row (Note that is this not the position)
-        int rowFirstAppIndex;
         // The index of the first visible row
         int rowIndex;
         // The offset of the first visible row
@@ -59,6 +57,7 @@
 
     private AlphabeticalAppsList mApps;
     private int mNumAppsPerRow;
+    private int mNumPredictedAppsPerRow;
 
     private Drawable mScrollbar;
     private Drawable mFastScrollerBg;
@@ -68,6 +67,7 @@
     private Paint mFastScrollTextPaint;
     private Rect mFastScrollTextBounds = new Rect();
     private float mFastScrollAlpha;
+    private int mPredictionBarHeight;
     private int mDownX;
     private int mDownY;
     private int mLastX;
@@ -123,8 +123,9 @@
     /**
      * Sets the number of apps per row in this recycler view.
      */
-    public void setNumAppsPerRow(int rowSize) {
-        mNumAppsPerRow = rowSize;
+    public void setNumAppsPerRow(int numAppsPerRow, int numPredictedAppsPerRow) {
+        mNumAppsPerRow = numAppsPerRow;
+        mNumPredictedAppsPerRow = numPredictedAppsPerRow;
     }
 
     @Override
@@ -134,6 +135,13 @@
     }
 
     /**
+     * Sets the prediction bar height.
+     */
+    public void setPredictionBarHeight(int height) {
+        mPredictionBarHeight = height;
+    }
+
+    /**
      * Sets the fast scroller alpha.
      */
     public void setFastScrollerAlpha(float alpha) {
@@ -330,6 +338,26 @@
             return "";
         }
 
+        // Stop the scroller if it is scrolling
+        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
+        stopScroll();
+
+        // If there is a prediction bar, then capture the appropriate area for the prediction bar
+        float predictionBarFraction = 0f;
+        if (mPredictionBarHeight > 0) {
+            predictionBarFraction = (float) mNumPredictedAppsPerRow / mApps.getSize();
+            if (touchFraction <= predictionBarFraction) {
+                // Scroll to the top of the view, where the prediction bar is
+                layoutManager.scrollToPositionWithOffset(0, 0);
+                updateScrollY(0);
+                return "";
+            }
+        }
+
+        // Since the app ranges are from 0..1, we need to map the touch fraction back to 0..1 from
+        // predictionBarFraction..1
+        touchFraction = (touchFraction - predictionBarFraction) *
+                (1f / (1f - predictionBarFraction));
         AlphabeticalAppsList.FastScrollSectionInfo lastScrollSection = fastScrollSections.get(0);
         for (int i = 1; i < fastScrollSections.size(); i++) {
             AlphabeticalAppsList.FastScrollSectionInfo scrollSection = fastScrollSections.get(i);
@@ -340,21 +368,19 @@
             lastScrollSection = scrollSection;
         }
 
-        // Scroll the position into view, anchored at the top of the screen if possible. We call the
-        // scroll method on the LayoutManager directly since it is not exposed by RecyclerView.
-        LinearLayoutManager layoutManager = (LinearLayoutManager) getLayoutManager();
-        stopScroll();
-        layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
-
-        // We need to workaround the RecyclerView to get the right scroll position after scrolling
+        // We need to workaround the RecyclerView to get the right scroll position
         List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
         getCurScrollState(mScrollPosState, items);
         if (mScrollPosState.rowIndex != -1) {
-            int rowIndex = findRowForAppIndex(mScrollPosState.rowFirstAppIndex);
-            int y = (rowIndex * mScrollPosState.rowHeight) - mScrollPosState.rowTopOffset;
-            updateScrollY(y);
+            int scrollY = getPaddingTop() + (mScrollPosState.rowIndex * mScrollPosState.rowHeight) +
+                    mPredictionBarHeight - mScrollPosState.rowTopOffset;
+            updateScrollY(scrollY);
         }
 
+        // Scroll to the view at the position, anchored at the top of the screen. We call the scroll
+        // method on the LayoutManager directly since it is not exposed by RecyclerView.
+        layoutManager.scrollToPositionWithOffset(lastScrollSection.appItem.position, 0);
+
         return lastScrollSection.sectionName;
     }
 
@@ -377,10 +403,9 @@
                 LAYOUT_DIRECTION_RTL);
         int rowCount = getNumRows();
         getCurScrollState(mScrollPosState, items);
-
         if (mScrollPosState.rowIndex != -1) {
             int height = getHeight() - getPaddingTop() - getPaddingBottom();
-            int totalScrollHeight = rowCount * mScrollPosState.rowHeight;
+            int totalScrollHeight = rowCount * mScrollPosState.rowHeight + mPredictionBarHeight;
             if (totalScrollHeight > height) {
                 int scrollbarHeight = Math.max(mScrollbarMinHeight,
                         (int) (height / ((float) totalScrollHeight / height)));
@@ -396,8 +421,8 @@
                 // that the user has already scrolled and then map that to the scroll bar bounds
                 int availableY = totalScrollHeight - height;
                 int availableScrollY = height - scrollbarHeight;
-                y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) -
-                        mScrollPosState.rowTopOffset;
+                y = (mScrollPosState.rowIndex * mScrollPosState.rowHeight) + mPredictionBarHeight
+                        - mScrollPosState.rowTopOffset;
                 y = getPaddingTop() +
                         (int) (((float) (getPaddingTop() + y) / availableY) * availableScrollY);
 
@@ -444,7 +469,6 @@
      */
     private void getCurScrollState(ScrollPositionState stateOut,
             List<AlphabeticalAppsList.AdapterItem> items) {
-        stateOut.rowFirstAppIndex = -1;
         stateOut.rowIndex = -1;
         stateOut.rowTopOffset = -1;
         stateOut.rowHeight = -1;
@@ -454,8 +478,7 @@
             int position = getChildPosition(child);
             if (position != NO_POSITION) {
                 AlphabeticalAppsList.AdapterItem item = items.get(position);
-                if (!item.isSectionHeader) {
-                    stateOut.rowFirstAppIndex = item.appIndex;
+                if (item.viewType == AppsGridAdapter.ICON_VIEW_TYPE) {
                     stateOut.rowIndex = findRowForAppIndex(item.appIndex);
                     stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child);
                     stateOut.rowHeight = child.getHeight();
diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java
index 5dac9f1..21dd7cb 100644
--- a/src/com/android/launcher3/AppsContainerView.java
+++ b/src/com/android/launcher3/AppsContainerView.java
@@ -26,12 +26,14 @@
 import android.text.TextWatcher;
 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.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
-import android.widget.EditText;
+import android.widget.FrameLayout;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import com.android.launcher3.util.Thunk;
@@ -44,11 +46,11 @@
  * The all apps view container.
  */
 public class AppsContainerView extends BaseContainerView implements DragSource, Insettable,
-        TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener,
-        View.OnClickListener, View.OnLongClickListener {
+        TextWatcher, TextView.OnEditorActionListener, LauncherTransitionable,
+        AlphabeticalAppsList.FilterChangedCallback, AppsGridAdapter.PredictionBarSpacerCallbacks,
+        View.OnTouchListener, View.OnClickListener, View.OnLongClickListener {
 
     public static final boolean GRID_MERGE_SECTIONS = true;
-    public static final boolean GRID_HIDE_SECTION_HEADERS = false;
 
     private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
     private static final boolean DYNAMIC_HEADER_ELEVATION = true;
@@ -64,12 +66,14 @@
 
     @Thunk Launcher mLauncher;
     @Thunk AlphabeticalAppsList mApps;
+    private LayoutInflater mLayoutInflater;
     private AppsGridAdapter mAdapter;
     private RecyclerView.LayoutManager mLayoutManager;
     private RecyclerView.ItemDecoration mItemDecoration;
 
-    private LinearLayout mContentView;
+    private FrameLayout mContentView;
     @Thunk AppsContainerRecyclerView mAppsRecyclerView;
+    private ViewGroup mPredictionBarView;
     private View mHeaderView;
     private View mSearchBarContainerView;
     private View mSearchButtonView;
@@ -77,11 +81,13 @@
     private AppsContainerSearchEditTextView mSearchBarEditView;
 
     private int mNumAppsPerRow;
+    private int mNumPredictedAppsPerRow;
     private Point mLastTouchDownPos = new Point(-1, -1);
     private Point mLastTouchPos = new Point();
     private int mContentMarginStart;
     // Normal container insets
     private int mContainerInset;
+    private int mPredictionBarHeight;
     // RecyclerView scroll position
     @Thunk int mRecyclerViewScrollY;
 
@@ -101,12 +107,17 @@
 
         mContainerInset = context.getResources().getDimensionPixelSize(
                 R.dimen.apps_container_inset);
+        mPredictionBarHeight = grid.allAppsCellHeightPx +
+                2 * res.getDimensionPixelSize(R.dimen.apps_prediction_icon_top_bottom_padding);
         mLauncher = (Launcher) context;
+        mLayoutInflater = LayoutInflater.from(context);
         mNumAppsPerRow = grid.appsViewNumCols;
-        mApps = new AlphabeticalAppsList(context, mNumAppsPerRow);
-        mAdapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, mLauncher, this);
+        mNumPredictedAppsPerRow = grid.appsViewNumPredictiveCols;
+        mApps = new AlphabeticalAppsList(context, this, mNumAppsPerRow, mNumPredictedAppsPerRow);
+        mAdapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this, this, mLauncher, this);
         mAdapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
         mAdapter.setNumAppsPerRow(mNumAppsPerRow);
+        mAdapter.setPredictionRowHeight(mPredictionBarHeight);
         mLayoutManager = mAdapter.getLayoutManager();
         mItemDecoration = mAdapter.getItemDecoration();
         mContentMarginStart = mAdapter.getContentMarginStart();
@@ -186,7 +197,7 @@
 
         // Work around the search box getting first focus and showing the cursor by
         // proxying the focus from the content view to the recycler view directly
-        mContentView = (LinearLayout) findViewById(R.id.apps_list);
+        mContentView = (FrameLayout) findViewById(R.id.apps_list);
         mContentView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
             @Override
             public void onFocusChange(View v, boolean hasFocus) {
@@ -195,12 +206,20 @@
                 }
             }
         });
+
+        // Fix the header view elevation if not dynamically calculating it
         mHeaderView = findViewById(R.id.header);
         mHeaderView.setOnClickListener(this);
         if (Utilities.isLmpOrAbove() && !DYNAMIC_HEADER_ELEVATION) {
             mHeaderView.setElevation(DynamicGrid.pxFromDp(HEADER_ELEVATION_DP,
                 getContext().getResources().getDisplayMetrics()));
         }
+
+        // Fix the prediction bar size
+        mPredictionBarView = (ViewGroup) findViewById(R.id.prediction_bar);
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
+        lp.height = mPredictionBarHeight;
+
         mSearchButtonView = mHeaderView.findViewById(R.id.search_button);
         mSearchBarContainerView = findViewById(R.id.app_search_container);
         mDismissSearchButtonView = mSearchBarContainerView.findViewById(R.id.dismiss_search_button);
@@ -227,7 +246,8 @@
         }
         mAppsRecyclerView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view);
         mAppsRecyclerView.setApps(mApps);
-        mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
+        mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
+        mAppsRecyclerView.setPredictionBarHeight(mPredictionBarHeight);
         mAppsRecyclerView.setLayoutManager(mLayoutManager);
         mAppsRecyclerView.setAdapter(mAdapter);
         mAppsRecyclerView.setHasFixedSize(true);
@@ -247,15 +267,52 @@
     }
 
     @Override
+    public void onBindPredictionBar() {
+        if (!updatePredictionBarVisibility()) {
+            return;
+        }
+
+        List<AppInfo> predictedApps = mApps.getPredictedApps();
+        int childCount = mPredictionBarView.getChildCount();
+        for (int i = 0; i < mNumPredictedAppsPerRow; i++) {
+            BubbleTextView icon;
+            if (i < childCount) {
+                // If a child at that index exists, then get that child
+                icon = (BubbleTextView) mPredictionBarView.getChildAt(i);
+            } else {
+                // Otherwise, inflate a new icon
+                icon = (BubbleTextView) mLayoutInflater.inflate(
+                        R.layout.apps_prediction_bar_icon_view, mPredictionBarView, false);
+                icon.setOnTouchListener(this);
+                icon.setOnClickListener(mLauncher);
+                icon.setOnLongClickListener(this);
+                icon.setFocusable(true);
+                mPredictionBarView.addView(icon);
+            }
+
+            // Either apply the app info to the child, or hide the view
+            if (i < predictedApps.size()) {
+                if (icon.getVisibility() != View.VISIBLE) {
+                    icon.setVisibility(View.VISIBLE);
+                }
+                icon.applyFromApplicationInfo(predictedApps.get(i));
+            } else {
+                icon.setVisibility(View.INVISIBLE);
+            }
+        }
+    }
+
+    @Override
     protected void onFixedBoundsUpdated() {
         // Update the number of items in the grid
         LauncherAppState app = LauncherAppState.getInstance();
         DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
         if (grid.updateAppsViewNumCols(getContext().getResources(), mFixedBounds.width())) {
             mNumAppsPerRow = grid.appsViewNumCols;
-            mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow);
+            mNumPredictedAppsPerRow = grid.appsViewNumPredictiveCols;
+            mAppsRecyclerView.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
             mAdapter.setNumAppsPerRow(mNumAppsPerRow);
-            mApps.setNumAppsPerRow(mNumAppsPerRow);
+            mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow);
         }
     }
 
@@ -297,10 +354,16 @@
 
         // Update the header bar
         if (hasSearchBar) {
-            LinearLayout.LayoutParams lp =
-                    (LinearLayout.LayoutParams) mHeaderView.getLayoutParams();
+            FrameLayout.LayoutParams lp =
+                    (FrameLayout.LayoutParams) mHeaderView.getLayoutParams();
             lp.leftMargin = lp.rightMargin = inset;
+            mHeaderView.requestLayout();
         }
+
+        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mPredictionBarView.getLayoutParams();
+        lp.leftMargin = inset + mAppsRecyclerView.getScrollbarWidth();
+        lp.rightMargin = inset + mAppsRecyclerView.getScrollbarWidth();
+        mPredictionBarView.requestLayout();
     }
 
     /**
@@ -499,7 +562,7 @@
             List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
             for (int i = 0; i < items.size(); i++) {
                 AlphabeticalAppsList.AdapterItem item = items.get(i);
-                if (!item.isSectionHeader) {
+                if (item.viewType == AppsGridAdapter.ICON_VIEW_TYPE) {
                     mAppsRecyclerView.getChildAt(i).performClick();
                     getInputMethodManager().hideSoftInputFromWindow(getWindowToken(), 0);
                     return true;
@@ -510,6 +573,11 @@
     }
 
     @Override
+    public void onFilterChanged() {
+        updatePredictionBarVisibility();
+    }
+
+    @Override
     public View getContent() {
         return null;
     }
@@ -554,6 +622,12 @@
                 mHeaderView.setElevation(newElevation);
             }
         }
+
+        // XXX: Optimize this, stop once we are out of bounds
+        if (mRecyclerViewScrollY < 0) {
+            new Throwable().printStackTrace();
+        }
+        mPredictionBarView.setTranslationY(-mRecyclerViewScrollY + mAppsRecyclerView.getPaddingTop());
     }
 
     /**
@@ -683,6 +757,21 @@
     }
 
     /**
+     * Updates the visibility of the prediction bar.
+     * @return whether the prediction bar is visible
+     */
+    private boolean updatePredictionBarVisibility() {
+        boolean showPredictionBar = !mApps.getPredictedApps().isEmpty() && (!mApps.hasFilter() ||
+                mSearchBarEditView.getEditableText().toString().isEmpty());
+        if (showPredictionBar) {
+            mPredictionBarView.setVisibility(View.VISIBLE);
+        } else if (!showPredictionBar) {
+            mPredictionBarView.setVisibility(View.INVISIBLE);
+        }
+        return showPredictionBar;
+    }
+
+    /**
      * Returns an input method manager.
      */
     private InputMethodManager getInputMethodManager() {
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
index 4014e38..dfbfa01 100644
--- a/src/com/android/launcher3/AppsGridAdapter.java
+++ b/src/com/android/launcher3/AppsGridAdapter.java
@@ -6,6 +6,7 @@
 import android.graphics.Paint;
 import android.graphics.PointF;
 import android.graphics.Rect;
+import android.os.Handler;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
@@ -26,21 +27,31 @@
     public static final String TAG = "AppsGridAdapter";
     private static final boolean DEBUG = false;
 
-    private static final int SECTION_BREAK_VIEW_TYPE = 0;
-    private static final int ICON_VIEW_TYPE = 1;
-    private static final int EMPTY_VIEW_TYPE = 2;
+    // A section break in the grid
+    public static final int SECTION_BREAK_VIEW_TYPE = 0;
+    // A normal icon
+    public static final int ICON_VIEW_TYPE = 1;
+    // The message shown when there are no filtered results
+    public static final int EMPTY_VIEW_TYPE = 2;
+    // The spacer used for the prediction bar
+    public static final int PREDICTION_BAR_SPACER_TYPE = 3;
+
+    /**
+     * Callback for when the prediction bar spacer is bound.
+     */
+    public interface PredictionBarSpacerCallbacks {
+        void onBindPredictionBar();
+    }
 
     /**
      * ViewHolder for each icon.
      */
     public static class ViewHolder extends RecyclerView.ViewHolder {
         public View mContent;
-        public boolean mIsEmptyRow;
 
-        public ViewHolder(View v, boolean isEmptyRow) {
+        public ViewHolder(View v) {
             super(v);
             mContent = v;
-            mIsEmptyRow = isEmptyRow;
         }
     }
 
@@ -61,8 +72,8 @@
                 return mAppsPerRow;
             }
 
-            if (mApps.getAdapterItems().get(position).isSectionHeader) {
-                // Section break spans full width
+            if (mApps.getAdapterItems().get(position).viewType != AppsGridAdapter.ICON_VIEW_TYPE) {
+                // Both the section breaks and predictive bar span the full width
                 return mAppsPerRow;
             } else {
                 return 1;
@@ -88,7 +99,7 @@
 
             DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
             List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
-            boolean hasDrawnPredictedAppDivider = false;
+            boolean hasDrawnPredictedAppsDivider = false;
             int childCount = parent.getChildCount();
             int lastSectionTop = 0;
             int lastSectionHeight = 0;
@@ -99,13 +110,13 @@
                     continue;
                 }
 
-                if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppDivider) {
-                    // Draw the divider under the predicted app
+                if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppsDivider) {
+                    // Draw the divider under the predicted apps
                     parent.getBackground().getPadding(mTmpBounds);
                     int top = child.getTop() + child.getHeight();
                     c.drawLine(mTmpBounds.left, top, parent.getWidth() - mTmpBounds.right, top,
                             mPredictedAppsDividerPaint);
-                    hasDrawnPredictedAppDivider = true;
+                    hasDrawnPredictedAppsDivider = true;
 
                 } else if (grid.isPhone() && shouldDrawItemSection(holder, i, items)) {
                     // At this point, we only draw sections for each section break;
@@ -220,9 +231,10 @@
         /**
          * Returns whether to draw the divider for a given child.
          */
-        private boolean shouldDrawItemDivider(ViewHolder holder, List<AlphabeticalAppsList.AdapterItem> items) {
+        private boolean shouldDrawItemDivider(ViewHolder holder,
+                List<AlphabeticalAppsList.AdapterItem> items) {
             int pos = holder.getPosition();
-            return items.get(pos).isPredictedApp;
+            return items.get(pos).viewType == AppsGridAdapter.PREDICTION_BAR_SPACER_TYPE;
         }
 
         /**
@@ -233,31 +245,27 @@
             int pos = holder.getPosition();
             AlphabeticalAppsList.AdapterItem item = items.get(pos);
 
-            // Ensure it's not an empty row
-            if (holder.mIsEmptyRow) {
-                return false;
-            }
-            // Ensure this is not a section break
-            if (item.isSectionHeader) {
-                return false;
-            }
-            // Ensure this is not a predicted app
-            if (item.isPredictedApp) {
+            // Ensure it's an icon
+            if (item.viewType != AppsGridAdapter.ICON_VIEW_TYPE) {
                 return false;
             }
             // Draw the section header for the first item in each section
-            return (childIndex == 0) || (items.get(pos - 1).isSectionHeader && !item.isSectionHeader);
+            return (childIndex == 0) ||
+                    (items.get(pos - 1).viewType == AppsGridAdapter.SECTION_BREAK_VIEW_TYPE);
         }
     }
 
+    private Handler mHandler;
     private LayoutInflater mLayoutInflater;
     @Thunk AlphabeticalAppsList mApps;
     private GridLayoutManager mGridLayoutMgr;
     private GridSpanSizer mGridSizer;
     private GridItemDecoration mItemDecoration;
+    private PredictionBarSpacerCallbacks mPredictionBarCb;
     private View.OnTouchListener mTouchListener;
     private View.OnClickListener mIconClickListener;
     private View.OnLongClickListener mIconLongClickListener;
+    @Thunk int mPredictionBarHeight;
     @Thunk int mAppsPerRow;
     @Thunk boolean mIsRtl;
     private String mEmptySearchText;
@@ -271,11 +279,13 @@
 
 
     public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
-            View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
-            View.OnLongClickListener iconLongClickListener) {
+            PredictionBarSpacerCallbacks pbCb, View.OnTouchListener touchListener,
+            View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
         Resources res = context.getResources();
+        mHandler = new Handler();
         mApps = apps;
         mAppsPerRow = appsPerRow;
+        mPredictionBarCb = pbCb;
         mGridSizer = new GridSpanSizer();
         mGridLayoutMgr = new GridLayoutManager(context, appsPerRow, GridLayoutManager.VERTICAL,
                 false);
@@ -310,6 +320,13 @@
     }
 
     /**
+     * Sets the prediction row height.
+     */
+    public void setPredictionRowHeight(int height) {
+        mPredictionBarHeight = height;
+    }
+
+    /**
      * Sets whether we are in RTL mode.
      */
     public void setRtl(boolean rtl) {
@@ -350,17 +367,24 @@
         switch (viewType) {
             case EMPTY_VIEW_TYPE:
                 return new ViewHolder(mLayoutInflater.inflate(R.layout.apps_empty_view, parent,
-                        false), true /* isEmptyRow */);
+                        false));
             case SECTION_BREAK_VIEW_TYPE:
-                return new ViewHolder(new View(parent.getContext()), false /* isEmptyRow */);
+                return new ViewHolder(new View(parent.getContext()));
+            case PREDICTION_BAR_SPACER_TYPE:
+                // Create a view of a specific height to match the floating prediction bar
+                View v = new View(parent.getContext());
+                ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT, mPredictionBarHeight);
+                v.setLayoutParams(lp);
+                return new ViewHolder(v);
             case ICON_VIEW_TYPE:
                 BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
-                        R.layout.apps_grid_row_icon_view, parent, false);
+                        R.layout.apps_grid_icon_view, parent, false);
                 icon.setOnTouchListener(mTouchListener);
                 icon.setOnClickListener(mIconClickListener);
                 icon.setOnLongClickListener(mIconLongClickListener);
                 icon.setFocusable(true);
-                return new ViewHolder(icon, false /* isEmptyRow */);
+                return new ViewHolder(icon);
             default:
                 throw new RuntimeException("Unexpected view type");
         }
@@ -374,6 +398,16 @@
                 BubbleTextView icon = (BubbleTextView) holder.mContent;
                 icon.applyFromApplicationInfo(info);
                 break;
+            case PREDICTION_BAR_SPACER_TYPE:
+                mHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mPredictionBarCb != null) {
+                            mPredictionBarCb.onBindPredictionBar();
+                        }
+                    }
+                });
+                break;
             case EMPTY_VIEW_TYPE:
                 TextView emptyViewText = (TextView) holder.mContent.findViewById(R.id.empty_text);
                 emptyViewText.setText(mEmptySearchText);
@@ -394,9 +428,9 @@
     public int getItemViewType(int position) {
         if (mApps.hasNoFilteredResults()) {
             return EMPTY_VIEW_TYPE;
-        } else if (mApps.getAdapterItems().get(position).isSectionHeader) {
-            return SECTION_BREAK_VIEW_TYPE;
         }
-        return ICON_VIEW_TYPE;
+
+        AlphabeticalAppsList.AdapterItem item = mApps.getAdapterItems().get(position);
+        return item.viewType;
     }
 }
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 3bbf0e7..dc6de07 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -127,6 +127,7 @@
     int allAppsNumRows;
     int allAppsNumCols;
     int appsViewNumCols;
+    int appsViewNumPredictiveCols;
     int searchBarSpaceWidthPx;
     int searchBarSpaceHeightPx;
     int pageIndicatorHeightPx;
@@ -411,7 +412,7 @@
 
         // All Apps
         allAppsCellWidthPx = allAppsIconSizePx;
-        allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
+        allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + allAppsIconTextSizePx;
         int maxLongEdgeCellCount =
                 res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
         int maxShortEdgeCellCount =
@@ -440,10 +441,13 @@
         int appsViewLeftMarginPx =
                 res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
         int availableAppsWidthPx = (containerWidth > 0) ? containerWidth : availableWidthPx;
-        int numCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
+        int numAppsCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
                 (allAppsCellWidthPx + 2 * allAppsCellPaddingPx);
-        if (numCols != appsViewNumCols) {
-            appsViewNumCols = numCols;
+        int numPredictiveAppCols = isPhone() ? numColumns : numAppsCols;
+        if ((numAppsCols != appsViewNumCols) ||
+                (numPredictiveAppCols != appsViewNumPredictiveCols)) {
+            appsViewNumCols = numAppsCols;
+            appsViewNumPredictiveCols = numPredictiveAppCols;
             return true;
         }
         return false;