Make sure pages are always accessible via left/right arrow keys.

- Handle NextPageFirstItem as first focusable item in reading order
- Handle PreviousPageLastItem as last focusable item in reading order
- Check the hotseat after the workspace in both cases above
- Dpad horizontal navigation (left/right) uses these as a last
  resort (Rule3) to guarantee an item takes focus if a page exists

Note that it is necessary to search for a focusable item because
widgets are not yet focusable.

Bug: 25591057
Change-Id: I953648bd76c657d660a38427fdd4108bf9963c23
diff --git a/src/com/android/launcher3/FocusHelper.java b/src/com/android/launcher3/FocusHelper.java
index 4709488..b243da3 100644
--- a/src/com/android/launcher3/FocusHelper.java
+++ b/src/com/android/launcher3/FocusHelper.java
@@ -374,7 +374,9 @@
         // Process the focus.
         int newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX,
                 countY, matrix, iconIndex, pageIndex, pageCount, Utilities.isRtl(v.getResources()));
+        boolean isRtl = Utilities.isRtl(v.getResources());
         View newIcon = null;
+        CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex);
         switch (newIconIndex) {
             case FocusLogic.NOOP:
                 if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
@@ -396,20 +398,30 @@
                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
                             matrix, FocusLogic.PIVOT, newPageIndex, pageCount,
                             Utilities.isRtl(v.getResources()));
-                    newIcon = parent.getChildAt(newIconIndex);
+                    if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
+                        newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
+                                isRtl);
+                    } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
+                        newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
+                                isRtl);
+                    } else {
+                        newIcon = parent.getChildAt(newIconIndex);
+                    }
                 }
                 break;
             case FocusLogic.PREVIOUS_PAGE_FIRST_ITEM:
-                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
-                newIcon = parent.getChildAt(0);
+                workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
+                newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
+                if (newIcon == null) {
+                    newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
+                    workspace.snapToPage(pageIndex - 1);
+                }
                 break;
             case FocusLogic.PREVIOUS_PAGE_LAST_ITEM:
-                parent = getCellLayoutChildrenForIndex(workspace, pageIndex - 1);
-                newIcon = parent.getChildAt(parent.getChildCount() - 1);
+                newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex, isRtl);
                 break;
             case FocusLogic.NEXT_PAGE_FIRST_ITEM:
-                parent = getCellLayoutChildrenForIndex(workspace, pageIndex + 1);
-                newIcon = parent.getChildAt(0);
+                newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex, isRtl);
                 break;
             case FocusLogic.NEXT_PAGE_LEFT_COLUMN:
             case FocusLogic.PREVIOUS_PAGE_LEFT_COLUMN:
@@ -425,14 +437,28 @@
                     newIconIndex = FocusLogic.handleKeyEvent(keyCode, countX + 1, countY,
                             matrix, FocusLogic.PIVOT, newPageIndex, pageCount,
                             Utilities.isRtl(v.getResources()));
-                    newIcon = parent.getChildAt(newIconIndex);
+                    if (newIconIndex == FocusLogic.NEXT_PAGE_FIRST_ITEM) {
+                        newIcon = handleNextPageFirstItem(workspace, hotseatLayout, pageIndex,
+                                isRtl);
+                    } else if (newIconIndex == FocusLogic.PREVIOUS_PAGE_LAST_ITEM) {
+                        newIcon = handlePreviousPageLastItem(workspace, hotseatLayout, pageIndex,
+                                isRtl);
+                    } else {
+                        newIcon = parent.getChildAt(newIconIndex);
+                    }
                 }
                 break;
             case FocusLogic.CURRENT_PAGE_FIRST_ITEM:
-                newIcon = parent.getChildAt(0);
+                newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
+                if (newIcon == null) {
+                    newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
+                }
                 break;
             case FocusLogic.CURRENT_PAGE_LAST_ITEM:
-                newIcon = parent.getChildAt(parent.getChildCount() - 1);
+                newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
+                if (newIcon == null) {
+                    newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout, isRtl);
+                }
                 break;
             default:
                 // current page, some item.
@@ -507,4 +533,55 @@
         return (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) &&
                 event.hasModifiers(KeyEvent.META_CTRL_ON);
     }
+
+    private static View handlePreviousPageLastItem(Workspace workspace, CellLayout hotseatLayout,
+            int pageIndex, boolean isRtl) {
+        CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex - 1);
+        View newIcon = getFirstFocusableIconInReverseReadingOrder(workspaceLayout, isRtl);
+        if (newIcon == null) {
+            newIcon = getFirstFocusableIconInReverseReadingOrder(hotseatLayout,isRtl);
+            workspace.snapToPage(pageIndex - 1);
+        }
+        return newIcon;
+    }
+
+    private static View handleNextPageFirstItem(Workspace workspace, CellLayout hotseatLayout,
+            int pageIndex, boolean isRtl) {
+        CellLayout workspaceLayout = (CellLayout) workspace.getChildAt(pageIndex + 1);
+        View newIcon = getFirstFocusableIconInReadingOrder(workspaceLayout, isRtl);
+        if (newIcon == null) {
+            newIcon = getFirstFocusableIconInReadingOrder(hotseatLayout, isRtl);
+            workspace.snapToPage(pageIndex + 1);
+        }
+        return newIcon;
+    }
+
+    private static View getFirstFocusableIconInReadingOrder(CellLayout cellLayout, boolean isRtl) {
+        View icon;
+        int countX = cellLayout.getCountX();
+        for (int y = 0; y < cellLayout.getCountY(); y++) {
+            int increment = isRtl ? -1 : 1;
+            for (int x = isRtl ? countX - 1 : 0; 0 <= x && x < countX; x += increment) {
+                if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
+                    return icon;
+                }
+            }
+        }
+        return null;
+    }
+
+    private static View getFirstFocusableIconInReverseReadingOrder(CellLayout cellLayout,
+            boolean isRtl) {
+        View icon;
+        int countX = cellLayout.getCountX();
+        for (int y = cellLayout.getCountY() - 1; y >= 0; y--) {
+            int increment = isRtl ? 1 : -1;
+            for (int x = isRtl ? 0 : countX - 1; 0 <= x && x < countX; x += increment) {
+                if ((icon = cellLayout.getChildAt(x, y)) != null && icon.isFocusable()) {
+                    return icon;
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/src/com/android/launcher3/util/FocusLogic.java b/src/com/android/launcher3/util/FocusLogic.java
index f56d162..22e0cad 100644
--- a/src/com/android/launcher3/util/FocusLogic.java
+++ b/src/com/android/launcher3/util/FocusLogic.java
@@ -92,7 +92,7 @@
         int newIndex = NOOP;
         switch (keyCode) {
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/);
+                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, -1 /*increment*/, isRtl);
                 if (!isRtl && newIndex == NOOP && pageIndex > 0) {
                     newIndex = PREVIOUS_PAGE_RIGHT_COLUMN;
                 } else if (isRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
@@ -100,7 +100,7 @@
                 }
                 break;
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/);
+                newIndex = handleDpadHorizontal(iconIdx, cntX, cntY, map, 1 /*increment*/, isRtl);
                 if (!isRtl && newIndex == NOOP && pageIndex < pageCount - 1) {
                     newIndex = NEXT_PAGE_LEFT_COLUMN;
                 } else if (isRtl && newIndex == NOOP && pageIndex > 0) {
@@ -314,7 +314,7 @@
      */
     // TODO: add unit tests to verify all permutation.
     private static int handleDpadHorizontal(int iconIdx, int cntX, int cntY,
-            int[][] matrix, int increment) {
+            int[][] matrix, int increment, boolean isRtl) {
         if(matrix == null) {
             throw new IllegalStateException("Dpad navigation requires a matrix.");
         }
@@ -370,17 +370,13 @@
             }
         }
 
-        // 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)
+        // Rule3: 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 (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
-                            && newIconIndex != ALL_APPS_COLUMN) {
-                        return newIconIndex;
-                    }
-                }
+            if (isRtl) {
+                return increment < 0 ? NEXT_PAGE_FIRST_ITEM : PREVIOUS_PAGE_LAST_ITEM;
             }
+            return increment < 0 ? PREVIOUS_PAGE_LAST_ITEM : NEXT_PAGE_FIRST_ITEM;
         }
         return newIconIndex;
     }