Exploring dense all apps layout.

- Disabling section headers in all apps on tablet layouts
- Fixing issue with predictions not showing on rotation
- Fixing issue with over-aggressive dismissing of keyboard & filtered app state
- Fixing issue where the container bounds were running straight up to the nav bar

Change-Id: I5a5a56afa75b50be96af4894bf785ffbb1b15fb3
diff --git a/src/com/android/launcher3/AlphabeticalAppsList.java b/src/com/android/launcher3/AlphabeticalAppsList.java
index 62cb237..de4edcb 100644
--- a/src/com/android/launcher3/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/AlphabeticalAppsList.java
@@ -3,6 +3,7 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.support.v7.widget.RecyclerView;
+import android.util.Log;
 import com.android.launcher3.compat.AlphabeticIndexCompat;
 import com.android.launcher3.compat.UserHandleCompat;
 import com.android.launcher3.compat.UserManagerCompat;
@@ -77,6 +78,9 @@
  */
 public class AlphabeticalAppsList {
 
+    public static final String TAG = "AlphabeticalAppsList";
+    private static final boolean DEBUG = false;
+
     /**
      * Info about a section in the alphabetic list
      */
@@ -162,11 +166,58 @@
      * A filter interface to limit the set of applications in the apps list.
      */
     public interface Filter {
-        public boolean retainApp(AppInfo info, String sectionName);
+        boolean retainApp(AppInfo info, String sectionName);
     }
 
-    // The maximum number of rows allowed in a merged section before we stop merging
-    private static final int MAX_ROWS_IN_MERGED_SECTION = 3;
+    /**
+     * Common interface for different merging strategies.
+     */
+    private interface MergeAlgorithm {
+        boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount);
+    }
+
+    /**
+     * The logic we use to merge sections on tablets.
+     */
+    private static class TabletMergeAlgorithm implements MergeAlgorithm {
+
+        @Override
+        public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) {
+            // Merge EVERYTHING
+            return true;
+        }
+    }
+
+    /**
+     * The logic we use to merge sections on phones.
+     */
+    private static class PhoneMergeAlgorithm implements MergeAlgorithm {
+
+        private int mMinAppsPerRow;
+        private int mMinRowsInMergedSection;
+        private int mMaxAllowableMerges;
+
+        public PhoneMergeAlgorithm(int minAppsPerRow, int minRowsInMergedSection, int maxNumMerges) {
+            mMinAppsPerRow = minAppsPerRow;
+            mMinRowsInMergedSection = minRowsInMergedSection;
+            mMaxAllowableMerges = maxNumMerges;
+        }
+
+        @Override
+        public boolean continueMerging(int sectionAppCount, int numAppsPerRow, int mergeCount) {
+            // Continue merging if the number of hanging apps on the final row is less than some
+            // fixed number (ragged), the merged rows has yet to exceed some minimum row count,
+            // and while the number of merged sections is less than some fixed number of merges
+            int rows = sectionAppCount / numAppsPerRow;
+            int cols = sectionAppCount % numAppsPerRow;
+            return (0 < cols && cols < mMinAppsPerRow) &&
+                    rows < mMinRowsInMergedSection &&
+                    mergeCount < mMaxAllowableMerges;
+        }
+    }
+
+    private static final int MIN_ROWS_IN_MERGED_SECTION_PHONE = 3;
+    private static final int MAX_NUM_MERGES_PHONE = 2;
 
     private List<AppInfo> mApps = new ArrayList<>();
     private List<AppInfo> mFilteredApps = new ArrayList<>();
@@ -174,13 +225,13 @@
     private List<SectionInfo> mSections = new ArrayList<>();
     private List<FastScrollSectionInfo> mFastScrollerSections = new ArrayList<>();
     private List<ComponentName> 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 int mNumAppsPerRow;
-    // The maximum number of section merges we allow at a given time before we stop merging
-    private int mMaxAllowableMerges = Integer.MAX_VALUE;
 
     public AlphabeticalAppsList(Context context, int numAppsPerRow) {
         mIndexer = new AlphabeticIndexCompat(context);
@@ -193,7 +244,16 @@
      */
     public void setNumAppsPerRow(int numAppsPerRow) {
         mNumAppsPerRow = numAppsPerRow;
-        mMaxAllowableMerges = (int) Math.ceil(numAppsPerRow / 2f);
+
+        // Update the merge algorithm
+        DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
+        if (grid.isPhone()) {
+            mMergeAlgorithm = new PhoneMergeAlgorithm((int) Math.ceil(numAppsPerRow / 2f),
+                    MIN_ROWS_IN_MERGED_SECTION_PHONE, MAX_NUM_MERGES_PHONE);
+        } else {
+            mMergeAlgorithm = new TabletMergeAlgorithm();
+        }
+
         onAppsUpdated();
     }
 
@@ -392,7 +452,15 @@
         for (int i = 0; i < numApps; i++) {
             boolean isPredictedApp = i < numPredictedApps;
             AppInfo info = allApps.get(i);
-            String sectionName = isPredictedApp ? "" : mIndexer.computeSectionName(info.title);
+            String sectionName = "";
+            if (!isPredictedApp) {
+                // Only cache section names from non-predicted apps
+                sectionName = mCachedSectionNames.get(info.title);
+                if (sectionName == null) {
+                    sectionName = mIndexer.computeSectionName(info.title);
+                    mCachedSectionNames.put(info.title, sectionName);
+                }
+            }
 
             // Check if we want to retain this app
             if (mFilter != null && !mFilter.retainApp(info, sectionName)) {
@@ -429,20 +497,14 @@
 
         // Go through each section and try and merge some of the sections
         if (AppsContainerView.GRID_MERGE_SECTIONS && !hasFilter()) {
-            int minNumAppsPerRow = (int) Math.ceil(mNumAppsPerRow / 2f);
             int sectionAppCount = 0;
             for (int i = 0; i < mSections.size(); i++) {
                 SectionInfo section = mSections.get(i);
                 sectionAppCount = section.numApps;
                 int mergeCount = 1;
 
-                // Merge rows if the last app in this section is in a column that is greater than
-                // 0, but less than the min number of apps per row.  In addition, apply the
-                // constraint to stop merging if the number of rows in the section is greater than
-                // some limit, and also if there are no lessons to merge.
-                while (0 < (sectionAppCount % mNumAppsPerRow) &&
-                        (sectionAppCount % mNumAppsPerRow) < minNumAppsPerRow &&
-                        (sectionAppCount / mNumAppsPerRow) < MAX_ROWS_IN_MERGED_SECTION &&
+                // Merge rows based on the current strategy
+                while (mMergeAlgorithm.continueMerging(sectionAppCount, mNumAppsPerRow, mergeCount) &&
                         (i + 1) < mSections.size()) {
                     SectionInfo nextSection = mSections.remove(i + 1);
 
@@ -465,10 +527,13 @@
                     }
                     section.numApps += nextSection.numApps;
                     sectionAppCount += nextSection.numApps;
-                    mergeCount++;
-                    if (mergeCount >= mMaxAllowableMerges) {
-                        break;
+
+                    if (DEBUG) {
+                        Log.d(TAG, "Merging: " + nextSection.firstAppItem.sectionName +
+                                " to " + section.firstAppItem.sectionName +
+                                " mergedNumRows: " + (sectionAppCount / mNumAppsPerRow));
                     }
+                    mergeCount++;
                 }
             }
         }
diff --git a/src/com/android/launcher3/AppsContainerView.java b/src/com/android/launcher3/AppsContainerView.java
index b8d30d0..8a5c660 100644
--- a/src/com/android/launcher3/AppsContainerView.java
+++ b/src/com/android/launcher3/AppsContainerView.java
@@ -213,7 +213,13 @@
                         new AppsContainerSearchEditTextView.OnBackKeyListener() {
                             @Override
                             public void onBackKey() {
-                                hideSearchField(true, true);
+                                // Only hide the search field if there is no query, or if there
+                                // are no filtered results
+                                String query = Utilities.trim(
+                                        mSearchBarEditView.getEditableText().toString());
+                                if (query.isEmpty() || mApps.hasNoFilteredResults()) {
+                                    hideSearchField(true, true);
+                                }
                             }
                         });
             }
@@ -277,15 +283,17 @@
         } else {
             // If there are fixed bounds, then we update the padding to reflect the fixed bounds.
             setPadding(mFixedBounds.left, mFixedBounds.top, getMeasuredWidth() - mFixedBounds.right,
-                    mInsets.bottom);
+                    mFixedBounds.bottom);
         }
 
         // Update the apps recycler view, inset it by the container inset as well
+        DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
+        int startMargin = grid.isPhone() ? mContentMarginStart : 0;
         int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
         if (isRtl) {
-            mAppsRecyclerView.setPadding(inset, inset, inset + mContentMarginStart, inset);
+            mAppsRecyclerView.setPadding(inset, inset, inset + startMargin, inset);
         } else {
-            mAppsRecyclerView.setPadding(inset + mContentMarginStart, inset, inset, inset);
+            mAppsRecyclerView.setPadding(inset + startMargin, inset, inset, inset);
         }
 
         // Update the header bar
diff --git a/src/com/android/launcher3/AppsGridAdapter.java b/src/com/android/launcher3/AppsGridAdapter.java
index 9ecb2ee..5630449 100644
--- a/src/com/android/launcher3/AppsGridAdapter.java
+++ b/src/com/android/launcher3/AppsGridAdapter.java
@@ -90,6 +90,7 @@
                 return;
             }
 
+            DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().getDeviceProfile();
             List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
             boolean hasDrawnPredictedAppDivider = false;
             int childCount = parent.getChildCount();
@@ -104,8 +105,6 @@
 
                 if (shouldDrawItemDivider(holder, items) && !hasDrawnPredictedAppDivider) {
                     // Draw the divider under the predicted app
-                    DeviceProfile grid = LauncherAppState.getInstance().getDynamicGrid().
-                            getDeviceProfile();
                     int top = child.getTop() + child.getHeight();
                     int left = parent.getPaddingLeft();
                     int right = parent.getWidth() - parent.getPaddingRight();
@@ -113,7 +112,7 @@
                     c.drawLine(left + iconInset, top, right - iconInset, top, mPredictedAppsDividerPaint);
                     hasDrawnPredictedAppDivider = true;
 
-                } else if (shouldDrawItemSection(holder, i, items)) {
+                } else if (grid.isPhone() && shouldDrawItemSection(holder, i, items)) {
                     // At this point, we only draw sections for each section break;
                     int viewTopOffset = (2 * child.getPaddingTop());
                     int pos = holder.getPosition();
@@ -132,7 +131,8 @@
                             continue;
                         }
 
-                        // Find the section code points
+
+                        // Find the section name bounds
                         PointF sectionBounds = getAndCacheSectionBounds(sectionName);
 
                         // Calculate where to draw the section
diff --git a/src/com/android/launcher3/BaseContainerView.java b/src/com/android/launcher3/BaseContainerView.java
index 2a84432..bd1c625 100644
--- a/src/com/android/launcher3/BaseContainerView.java
+++ b/src/com/android/launcher3/BaseContainerView.java
@@ -59,11 +59,12 @@
             mFixedBounds.set(fixedBounds);
             if (Launcher.DISABLE_ALL_APPS_SEARCH_INTEGRATION) {
                 mFixedBounds.top = mInsets.top;
-                mFixedBounds.bottom = getMeasuredHeight();
+                mFixedBounds.bottom = mInsets.bottom;
             }
             // To ensure that the child RecyclerView has the full width to handle touches right to
             // the edge of the screen, we only apply the top and bottom padding to the bounds
-            mFixedBounds.inset(0, mFixedBoundsContainerInset);
+            mFixedBounds.top += mFixedBoundsContainerInset;
+            mFixedBounds.bottom += mFixedBoundsContainerInset;
             onFixedBoundsUpdated();
         }
         // Post the updates since they can trigger a relayout, and this call can be triggered from
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 16e4ce6..a289fce 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -350,6 +350,9 @@
 
     private DeviceProfile mDeviceProfile;
 
+    // This is set to the view that launched the activity that navigated the user away from
+    // launcher. Since there is no callback for when the activity has finished launching, enable
+    // the press state and keep this reference to reset the press state when we return to launcher.
     private BubbleTextView mWaitingForResume;
 
     protected static HashMap<String, CustomAppWidget> sCustomAppWidgets =
@@ -1021,10 +1024,12 @@
         if (mOnResumeState == State.WORKSPACE) {
             showWorkspace(false);
         } else if (mOnResumeState == State.APPS) {
+            boolean launchedFromApp = (mWaitingForResume != null);
             // Don't update the predicted apps if the user is returning to launcher in the apps
-            // view as they may be depending on the UI to be static to switch to another app
+            // view after launching an app, as they may be depending on the UI to be static to
+            // switch to another app, otherwise, if it was
             showAppsView(false /* animated */, false /* resetListToTop */,
-                    false /* updatePredictedApps */);
+                    !launchedFromApp /* updatePredictedApps */);
         } else if (mOnResumeState == State.WIDGETS) {
             showWidgetsView(false, false);
         }
diff --git a/src/com/android/launcher3/widget/WidgetsContainerView.java b/src/com/android/launcher3/widget/WidgetsContainerView.java
index 439227f..f8d7d92 100644
--- a/src/com/android/launcher3/widget/WidgetsContainerView.java
+++ b/src/com/android/launcher3/widget/WidgetsContainerView.java
@@ -373,7 +373,7 @@
         } else {
             // If there are fixed bounds, then we update the padding to reflect the fixed bounds.
             setPadding(mFixedBounds.left, mFixedBounds.top, getMeasuredWidth() - mFixedBounds.right,
-                    mInsets.bottom);
+                    mFixedBounds.bottom);
         }
     }