Add special column for All Apps button in FocusLogic's sparse matrix.

The All Apps button creates a number of edge cases, mainly because it
causes the hotseat to sometimes have an extra column than the workspace.
Previously, we sort of swept these problems under the rug by simply
ignoring the All Apps button if other icons were present in the hotseat,
with the assumption that those other icons should get focus instead of
the All Apps button. (If possible, we want to stay in the same column
when moving from the workspace to the hotseat.) But this doesn't always
work, as in the attached bug where the hotseat doesn't get focus at all
when the All Apps button is an obvious candidate for it.

By adding a specialized column in the focus matrix for the All Apps
button, we ensure that moving down to the hotseat stays within the
original column when possible, while also allowing the focus to switch
to the All Apps button if appropriate. Furthermore, we take care to skip
over the All Apps column when necessary in order to maintain all
previous functionality.

Bug: 25590522
Change-Id: I5d6a8ee69de8834314c4689246fe7d54329b2eef
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index 8516afb..3d12aa3 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -223,20 +223,18 @@
         if (keyCode == KeyEvent.KEYCODE_DPAD_UP &&
                 !profile.isVerticalBarLayout()) {
             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
-                    true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank,
-                    iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */);
+                    true /* hotseat horizontal */, profile.inv.hotseatAllAppsRank);
             iconIndex += iconParent.getChildCount();
-            countX = iconLayout.getCountX();
+            countX = hotseatLayout.getCountX();
             countY = iconLayout.getCountY() + hotseatLayout.getCountY();
             parent = iconParent;
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT &&
                 profile.isVerticalBarLayout()) {
             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout,
-                    false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank,
-                    iconRank == profile.inv.hotseatAllAppsRank /* include all apps icon */);
+                    false /* hotseat horizontal */, profile.inv.hotseatAllAppsRank);
             iconIndex += iconParent.getChildCount();
             countX = iconLayout.getCountX() + hotseatLayout.getCountX();
-            countY = iconLayout.getCountY();
+            countY = hotseatLayout.getCountY();
             parent = iconParent;
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
                 profile.isVerticalBarLayout()) {
@@ -326,15 +324,15 @@
         // with the hotseat.
         if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN && !profile.isVerticalBarLayout()) {
             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, true /* horizontal */,
-                    profile.inv.hotseatAllAppsRank,
-                    !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
-            countY = countY + 1;
+                    profile.inv.hotseatAllAppsRank);
+            countX = hotseatLayout.getCountX();
+            countY = countY + hotseatLayout.getCountY();
         } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT &&
                 profile.isVerticalBarLayout()) {
             matrix = FocusLogic.createSparseMatrix(iconLayout, hotseatLayout, false /* horizontal */,
-                    profile.inv.hotseatAllAppsRank,
-                    !hotseat.hasIcons() /* ignore all apps icon, unless there are no other icons */);
-            countX = countX + 1;
+                    profile.inv.hotseatAllAppsRank);
+            countX = countX + hotseatLayout.getCountX();
+            countY = hotseatLayout.getCountY();
         } else if (isUninstallKeyChord(e)) {
             matrix = FocusLogic.createSparseMatrix(iconLayout);
             if (UninstallDropTarget.supportsDrop(launcher, itemInfo)) {
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 902b6ec..3e83876 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -70,7 +70,7 @@
     public void setOnLongClickListener(OnLongClickListener l) {
         mContent.setOnLongClickListener(l);
     }
-  
+
     /* Get the orientation invariant order of the item in the hotseat for persistence. */
     int getOrderInHotseat(int x, int y) {
         return mHasVerticalHotseat ? (mContent.getCountY() - y - 1) : x;
diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java
index 2aae3c0..f56d162 100644
--- a/src/com/android/launcher3/util/FocusLogic.java
+++ b/src/com/android/launcher3/util/FocusLogic.java
@@ -63,6 +63,8 @@
     public static final int NEXT_PAGE_LEFT_COLUMN       = -9;
     public static final int NEXT_PAGE_RIGHT_COLUMN      = -10;
 
+    public static final int ALL_APPS_COLUMN = -11;
+
     // Matrix related constant.
     public static final int EMPTY = -1;
     public static final int PIVOT = 100;
@@ -186,22 +188,36 @@
      */
     // TODO: get rid of the dynamic matrix creation
     public static int[][] createSparseMatrix(CellLayout iconLayout, CellLayout hotseatLayout,
-            boolean isHorizontal, int allappsiconRank, boolean includeAllappsicon) {
+            boolean isHotseatHorizontal, int allappsiconRank) {
 
         ViewGroup iconParent = iconLayout.getShortcutsAndWidgets();
         ViewGroup hotseatParent = hotseatLayout.getShortcutsAndWidgets();
 
+        boolean moreIconsInHotseatThanWorkspace = isHotseatHorizontal ?
+                hotseatLayout.getCountX() > iconLayout.getCountX() :
+                hotseatLayout.getCountY() > iconLayout.getCountY();
+
         int m, n;
-        if (isHorizontal) {
-            m = iconLayout.getCountX();
+        if (isHotseatHorizontal) {
+            m = hotseatLayout.getCountX();
             n = iconLayout.getCountY() + hotseatLayout.getCountY();
         } else {
             m = iconLayout.getCountX() + hotseatLayout.getCountX();
-            n = iconLayout.getCountY();
+            n = hotseatLayout.getCountY();
         }
         int[][] matrix = createFullMatrix(m, n);
-
-        // Iterate thru the children of the top parent.
+        if (moreIconsInHotseatThanWorkspace) {
+            if (isHotseatHorizontal) {
+                for (int j = 0; j < n; j++) {
+                    matrix[allappsiconRank][j] = ALL_APPS_COLUMN;
+                }
+            } else {
+                for (int j = 0; j < m; j++) {
+                    matrix[j][allappsiconRank] = ALL_APPS_COLUMN;
+                }
+            }
+        }
+        // Iterate thru the children of the workspace.
         for (int i = 0; i < iconParent.getChildCount(); i++) {
             View cell = iconParent.getChildAt(i);
             if (!cell.isFocusable()) {
@@ -209,31 +225,29 @@
             }
             int cx = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellX;
             int cy = ((CellLayout.LayoutParams) cell.getLayoutParams()).cellY;
+            if (moreIconsInHotseatThanWorkspace) {
+                if (isHotseatHorizontal && cx >= allappsiconRank) {
+                    // Add 1 to account for the All Apps button.
+                    cx++;
+                }
+                if (!isHotseatHorizontal && cy >= allappsiconRank) {
+                    // Add 1 to account for the All Apps button.
+                    cy++;
+                }
+            }
             matrix[cx][cy] = i;
         }
 
-        // Iterate thru the children of the bottom parent
-        // The hotseat view group contains one more item than iconLayout column count.
-        // If {@param allappsiconRank} not negative, then the last icon in the hotseat
-        // is truncated. If it is negative, then all apps icon index is not inserted.
-        for(int i = hotseatParent.getChildCount() - 1; i >= (includeAllappsicon ? 0 : 1); i--) {
-            int delta = 0;
-            if (isHorizontal) {
+        // Iterate thru the children of the hotseat.
+        for (int i = hotseatParent.getChildCount() - 1; i >= 0; i--) {
+            if (isHotseatHorizontal) {
                 int cx = ((CellLayout.LayoutParams)
                         hotseatParent.getChildAt(i).getLayoutParams()).cellX;
-                if ((includeAllappsicon && cx >= allappsiconRank) ||
-                        (!includeAllappsicon && cx > allappsiconRank)) {
-                        delta = -1;
-                }
-                matrix[cx + delta][iconLayout.getCountY()] = iconParent.getChildCount() + i;
+                matrix[cx][iconLayout.getCountY()] = iconParent.getChildCount() + i;
             } else {
                 int cy = ((CellLayout.LayoutParams)
                         hotseatParent.getChildAt(i).getLayoutParams()).cellY;
-                if ((includeAllappsicon && cy >= allappsiconRank) ||
-                        (!includeAllappsicon && cy > allappsiconRank)) {
-                        delta = -1;
-                }
-                matrix[iconLayout.getCountX()][cy + delta] = iconParent.getChildCount() + i;
+                matrix[iconLayout.getCountX()][cy] = iconParent.getChildCount() + i;
             }
         }
         if (DEBUG) {
@@ -323,8 +337,9 @@
         }
 
         // Rule1: check first in the horizontal direction
-        for (int i = xPos + increment; 0 <= i && i < cntX; i = i + increment) {
-            if ((newIconIndex = inspectMatrix(i, yPos, cntX, cntY, matrix)) != NOOP) {
+        for (int x = xPos + increment; 0 <= x && x < cntX; x += increment) {
+            if ((newIconIndex = inspectMatrix(x, yPos, cntX, cntY, matrix)) != NOOP
+                    && newIconIndex != ALL_APPS_COLUMN) {
                 return newIconIndex;
             }
         }
@@ -333,15 +348,23 @@
         //              (x2-n, yPos + 2*increment), (x2-n, yPos - 2*increment)
         int nextYPos1;
         int nextYPos2;
-        int i = -1;
+        int x = -1;
         for (int coeff = 1; coeff < cntY; coeff++) {
             nextYPos1 = yPos + coeff * increment;
             nextYPos2 = yPos - coeff * increment;
-            for (i = xPos + increment * coeff; 0 <= i && i < cntX; i = i + increment) {
-                if ((newIconIndex = inspectMatrix(i, nextYPos1, cntX, cntY, matrix)) != NOOP) {
+            x = xPos + increment * coeff;
+            if (inspectMatrix(x, nextYPos1, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
+                nextYPos1 += increment;
+
+            }
+            if (inspectMatrix(x, nextYPos2, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
+                nextYPos2 -= increment;
+            }
+            for (; 0 <= x && x < cntX; x += increment) {
+                if ((newIconIndex = inspectMatrix(x, nextYPos1, cntX, cntY, matrix)) != NOOP) {
                     return newIconIndex;
                 }
-                if ((newIconIndex = inspectMatrix(i, nextYPos2, cntX, cntY, matrix)) != NOOP) {
+                if ((newIconIndex = inspectMatrix(x, nextYPos2, cntX, cntY, matrix)) != NOOP) {
                     return newIconIndex;
                 }
             }
@@ -350,9 +373,10 @@
         // Rule 3: if switching between pages, do a brute-force search to find an item that was
         //         missed by rules 1 and 2 (such as when going from a bottom right icon to top left)
         if (iconIdx == PIVOT) {
-            for (int x = xPos + increment; 0 <= x && x < cntX; x = x + increment) {
+            for (x = xPos + increment; 0 <= x && x < cntX; x += increment) {
                 for (int y = 0; y < cntY; y++) {
-                    if ((newIconIndex = inspectMatrix(x, y, cntX, cntY, matrix)) != NOOP) {
+                    if ((newIconIndex = inspectMatrix(x, y, cntX, cntY, matrix)) != NOOP
+                            && newIconIndex != ALL_APPS_COLUMN) {
                         return newIconIndex;
                     }
                 }
@@ -396,8 +420,9 @@
         }
 
         // Rule1: check first in the dpad direction
-        for (int j = yPos + increment; 0 <= j && j <cntY && 0 <= j; j = j + increment) {
-            if ((newIconIndex = inspectMatrix(xPos, j, cntX, cntY, matrix)) != NOOP) {
+        for (int y = yPos + increment; 0 <= y && y <cntY && 0 <= y; y += increment) {
+            if ((newIconIndex = inspectMatrix(xPos, y, cntX, cntY, matrix)) != NOOP
+                    && newIconIndex != ALL_APPS_COLUMN) {
                 return newIconIndex;
             }
         }
@@ -406,15 +431,23 @@
         //              (xPos + 2*increment, y_(2-n))), (xPos - 2*increment, y_(2-n))
         int nextXPos1;
         int nextXPos2;
-        int j = -1;
+        int y = -1;
         for (int coeff = 1; coeff < cntX; coeff++) {
             nextXPos1 = xPos + coeff * increment;
             nextXPos2 = xPos - coeff * increment;
-            for (j = yPos + increment * coeff; 0 <= j && j < cntY; j = j + increment) {
-                if ((newIconIndex = inspectMatrix(nextXPos1, j, cntX, cntY, matrix)) != NOOP) {
+            y = yPos + increment * coeff;
+            if (inspectMatrix(nextXPos1, y, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
+                nextXPos1 += increment;
+
+            }
+            if (inspectMatrix(nextXPos2, y, cntX, cntY, matrix) == ALL_APPS_COLUMN) {
+                nextXPos2 -= increment;
+            }
+            for (; 0 <= y && y < cntY; y = y + increment) {
+                if ((newIconIndex = inspectMatrix(nextXPos1, y, cntX, cntY, matrix)) != NOOP) {
                     return newIconIndex;
                 }
-                if ((newIconIndex = inspectMatrix(nextXPos2, j, cntX, cntY, matrix)) != NOOP) {
+                if ((newIconIndex = inspectMatrix(nextXPos2, y, cntX, cntY, matrix)) != NOOP) {
                     return newIconIndex;
                 }
             }
@@ -481,6 +514,7 @@
             case CURRENT_PAGE_LAST_ITEM:    return "CURRENT_PAGE_LAST";
             case NEXT_PAGE_FIRST_ITEM:      return "NEXT_PAGE_FIRST";
             case NEXT_PAGE_LEFT_COLUMN:     return "NEXT_PAGE_LEFT_COLUMN";
+            case ALL_APPS_COLUMN:           return "ALL_APPS_COLUMN";
             default:
                 return Integer.toString(index);
         }
diff --git a/tests/src/com/android/launcher3/util/FocusLogicTest.java b/tests/src/com/android/launcher3/util/FocusLogicTest.java
index 2c2f0d3..f93e913 100644
--- a/tests/src/com/android/launcher3/util/FocusLogicTest.java
+++ b/tests/src/com/android/launcher3/util/FocusLogicTest.java
@@ -19,6 +19,7 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.KeyEvent;
+import android.view.View;
 
 import com.android.launcher3.util.FocusLogic;
 
@@ -82,6 +83,155 @@
         assertEquals(0, i);
     }
 
+    public void testMoveIntoHotseatWithEqualHotseatAndWorkspaceColumns() {
+        // Test going from an icon right above the All Apps button to the All Apps button.
+        int[][] map = transpose(new int[][] {
+                {-1, -1, -1, -1, -1},
+                {-1, -1, -1, -1, -1},
+                {-1, -1, -1, -1, -1},
+                {-1, -1,  0, -1, -1},
+                { 2,  3,  1,  4,  5},
+        });
+        int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 5, 5, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test going from an icon above and to the right of the All Apps
+        // button to an icon to the right of the All Apps button.
+        map = transpose(new int[][] {
+                {-1, -1, -1, -1, -1},
+                {-1, -1, -1, -1, -1},
+                {-1, -1, -1, -1, -1},
+                {-1, -1, -1,  0, -1},
+                { 2,  3,  1,  4,  5},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 5, 5, map, 0, 1, 1, true);
+        assertEquals(4, i);
+    }
+
+    public void testMoveIntoHotseatWithExtraColumnForAllApps() {
+        // Test going from an icon above and to the left
+        // of the All Apps button to the All Apps button.
+        int[][] map = transpose(new int[][] {
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1,  0,-11, -1, -1, -1},
+                {-1, -1, -1,  1,  1, -1, -1},
+        });
+        int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test going from an icon above and to the right
+        // of the All Apps button to the All Apps button.
+        map = transpose(new int[][] {
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11,  0, -1, -1},
+                {-1, -1, -1,  1, -1, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test going from the All Apps button to an icon
+        // above and to the right of the All Apps button.
+        map = transpose(new int[][] {
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11,  0, -1, -1},
+                {-1, -1, -1,  1, -1, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_UP, 7, 6, map, 1, 1, 1, true);
+        assertEquals(0, i);
+        // Test going from an icon above and to the left of the
+        // All Apps button in landscape to the All Apps button.
+        map = transpose(new int[][] {
+                { -1, -1, -1, -1, -1},
+                { -1, -1, -1,  0, -1},
+                {-11,-11,-11,-11,  1},
+                { -1, -1, -1, -1, -1},
+                { -1, -1, -1, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 5, 5, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test going from the All Apps button in landscape to
+        // an icon above and to the left of the All Apps button.
+        map = transpose(new int[][] {
+                { -1, -1, -1, -1, -1},
+                { -1, -1, -1,  0, -1},
+                {-11,-11,-11,-11,  1},
+                { -1, -1, -1, -1, -1},
+                { -1, -1, -1, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 5, 5, map, 1, 1, 1, true);
+        assertEquals(0, i);
+        // Test that going to the hotseat always goes to the same row as the original icon.
+        map = transpose(new int[][]{
+                { 0,  1,  2,-11,  3,  4,  5},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                {-1, -1, -1,-11, -1, -1, -1},
+                { 7,  8,  9,  6, 10, 11, 12},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 0, 1, 1, true);
+        assertEquals(7, i);
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 1, 1, 1, true);
+        assertEquals(8, i);
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 2, 1, 1, true);
+        assertEquals(9, i);
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 3, 1, 1, true);
+        assertEquals(10, i);
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 4, 1, 1, true);
+        assertEquals(11, i);
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 7, 6, map, 5, 1, 1, true);
+        assertEquals(12, i);
+    }
+
+    public void testCrossingAllAppsColumn() {
+        // Test crossing from left to right in portrait.
+        int[][] map = transpose(new int[][] {
+                {-1, -1,-11, -1, -1},
+                {-1,  0,-11, -1, -1},
+                {-1, -1,-11,  1, -1},
+                {-1, -1,-11, -1, -1},
+                {-1, -1,  2, -1, -1},
+        });
+        int i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 5, 5, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test crossing from right to left in portrait.
+        map = transpose(new int[][] {
+                {-1, -1,-11, -1, -1},
+                {-1, -1,-11,  0, -1},
+                {-1,  1,-11, -1, -1},
+                {-1, -1,-11, -1, -1},
+                {-1, -1,  2, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_DOWN, 5, 5, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test crossing from left to right in landscape.
+        map = transpose(new int[][] {
+                { -1, -1, -1, -1, -1},
+                { -1, -1, -1,  0, -1},
+                {-11,-11,-11,-11,  2},
+                { -1,  1, -1, -1, -1},
+                { -1, -1, -1, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 5, 5, map, 0, 1, 1, true);
+        assertEquals(1, i);
+        // Test crossing from right to left in landscape.
+        map = transpose(new int[][] {
+                { -1, -1, -1, -1, -1},
+                { -1,  0, -1, -1, -1},
+                {-11,-11,-11,-11,  2},
+                { -1, -1,  1, -1, -1},
+                { -1, -1, -1, -1, -1},
+        });
+        i = FocusLogic.handleKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 5, 5, map, 0, 1, 1, true);
+        assertEquals(1, i);
+    }
+
     /** Transposes the matrix so that we can write it in human-readable format in the tests. */
     private int[][] transpose(int[][] m) {
         int[][] t = new int[m[0].length][m.length];