Merge "Navigate grid overview via arrows and tab properly." into main
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
new file mode 100644
index 0000000..98d363e
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+
+import com.android.launcher3.util.IntArray;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Helper class for navigating RecentsView grid tasks via arrow keys and tab.
+ */
+public class TaskGridNavHelper {
+    public static final int CLEAR_ALL_PLACEHOLDER_ID = -1;
+    public static final int INVALID_FOCUSED_TASK_ID = -1;
+
+    public static final int DIRECTION_UP = 0;
+    public static final int DIRECTION_DOWN = 1;
+    public static final int DIRECTION_LEFT = 2;
+    public static final int DIRECTION_RIGHT = 3;
+    public static final int DIRECTION_TAB = 4;
+
+    @Retention(SOURCE)
+    @IntDef({DIRECTION_UP, DIRECTION_DOWN, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_TAB})
+    public @interface TASK_NAV_DIRECTION {}
+
+    private final IntArray mOriginalTopRowIds;
+    private IntArray mTopRowIds;
+    private IntArray mBottomRowIds;
+    private final int mFocusedTaskId;
+
+    public TaskGridNavHelper(IntArray topIds, IntArray bottomIds, int focusedTaskId) {
+        mFocusedTaskId = focusedTaskId;
+        mOriginalTopRowIds = topIds.clone();
+        generateTaskViewIdGrid(topIds, bottomIds);
+    }
+
+    private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray) {
+        boolean hasFocusedTask = mFocusedTaskId != INVALID_FOCUSED_TASK_ID;
+        int maxSize =
+                Math.max(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0);
+        int minSize =
+                Math.min(topRowIdArray.size(), bottomRowIdArray.size()) + (hasFocusedTask ? 1 : 0);
+
+        // Add the focused task to the beginning of both arrays if it exists.
+        if (hasFocusedTask) {
+            topRowIdArray.add(0, mFocusedTaskId);
+            bottomRowIdArray.add(0, mFocusedTaskId);
+        }
+
+        // Fill in the shorter array with the ids from the longer one.
+        for (int i = minSize; i < maxSize; i++) {
+            if (i >= topRowIdArray.size()) {
+                topRowIdArray.add(bottomRowIdArray.get(i));
+            } else {
+                bottomRowIdArray.add(topRowIdArray.get(i));
+            }
+        }
+
+        // Add the clear all button to the end of both arrays
+        topRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
+        bottomRowIdArray.add(CLEAR_ALL_PLACEHOLDER_ID);
+
+        mTopRowIds = topRowIdArray;
+        mBottomRowIds = bottomRowIdArray;
+    }
+
+    /**
+     * Returns the id of the next page in the grid or -1 for the clear all button.
+     */
+    public int getNextGridPage(int currentPageTaskViewId, int delta,
+            @TASK_NAV_DIRECTION int direction, boolean cycle) {
+        boolean inTop = mTopRowIds.contains(currentPageTaskViewId);
+        int index = inTop ? mTopRowIds.indexOf(currentPageTaskViewId)
+                : mBottomRowIds.indexOf(currentPageTaskViewId);
+        int maxSize = Math.max(mTopRowIds.size(), mBottomRowIds.size());
+        int nextIndex = index + delta;
+
+        switch (direction) {
+            case DIRECTION_UP:
+            case DIRECTION_DOWN: {
+                return inTop ? mBottomRowIds.get(index) : mTopRowIds.get(index);
+            }
+            case DIRECTION_LEFT: {
+                int boundedIndex = cycle ? nextIndex % maxSize : Math.min(nextIndex, maxSize - 1);
+                return inTop ? mTopRowIds.get(boundedIndex)
+                        : mBottomRowIds.get(boundedIndex);
+            }
+            case DIRECTION_RIGHT: {
+                int boundedIndex =
+                        cycle ? (nextIndex < 0 ? maxSize - 1 : nextIndex) : Math.max(
+                                nextIndex, 0);
+                boolean inOriginalTop = mOriginalTopRowIds.contains(currentPageTaskViewId);
+                return inOriginalTop ? mTopRowIds.get(boundedIndex)
+                        : mBottomRowIds.get(boundedIndex);
+            }
+            case DIRECTION_TAB: {
+                int boundedIndex =
+                        cycle ? nextIndex < 0 ? maxSize - 1 : nextIndex % maxSize : Math.min(
+                                nextIndex, maxSize - 1);
+                if (delta >= 0) {
+                    return inTop && mTopRowIds.get(index) != mBottomRowIds.get(index)
+                            ? mBottomRowIds.get(index)
+                            : mTopRowIds.get(boundedIndex);
+                } else {
+                    if (mTopRowIds.contains(currentPageTaskViewId)) {
+                        return mBottomRowIds.get(boundedIndex);
+                    } else {
+                        // Go up to top if there is task above
+                        return mTopRowIds.get(index) != mBottomRowIds.get(index)
+                                ? mTopRowIds.get(index)
+                                : mBottomRowIds.get(boundedIndex);
+                    }
+                }
+            }
+            default:
+                return currentPageTaskViewId;
+        }
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9769d6b..6235490 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -55,6 +55,11 @@
 import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
 import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId;
 import static com.android.quickstep.util.LogUtils.splitFailureMessage;
+import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN;
+import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT;
+import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT;
+import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB;
+import static com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP;
 import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA;
 import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
 import static com.android.quickstep.views.OverviewActionsView.FLAG_IS_NOT_TABLET;
@@ -196,6 +201,7 @@
 import com.android.quickstep.util.SplitSelectStateController;
 import com.android.quickstep.util.SurfaceTransaction;
 import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskGridNavHelper;
 import com.android.quickstep.util.TaskViewSimulator;
 import com.android.quickstep.util.TaskVisualsChangeListener;
 import com.android.quickstep.util.TransformParams;
@@ -4065,12 +4071,19 @@
         return anim;
     }
 
-    private boolean snapToPageRelative(int pageCount, int delta, boolean cycle) {
+    private boolean snapToPageRelative(int delta, boolean cycle,
+            @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
+        // Ignore grid page snap events while scroll animations are running, otherwise the next
+        // page gets set before the animation finishes and can cause jumps.
+        if (!mScroller.isFinished()) {
+            return true;
+        }
+        int pageCount = getPageCount();
         if (pageCount == 0) {
             return false;
         }
-        final int newPageUnbound = getNextPage() + delta;
-        if (!cycle && (newPageUnbound < 0 || newPageUnbound >= pageCount)) {
+        final int newPageUnbound = getNextPageInternal(delta, direction, cycle);
+        if (!cycle && (newPageUnbound < 0 || newPageUnbound > pageCount)) {
             return false;
         }
         snapToPage((newPageUnbound + pageCount) % pageCount);
@@ -4078,6 +4091,34 @@
         return true;
     }
 
+    private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
+            boolean cycle) {
+        if (!showAsGrid()) {
+            return getNextPage() + delta;
+        }
+
+        // Init task grid nav helper with top/bottom id arrays.
+        TaskGridNavHelper taskGridNavHelper = new TaskGridNavHelper(getTopRowIdArray(),
+                getBottomRowIdArray(), mFocusedTaskViewId);
+
+        // Get current page's task view ID.
+        TaskView currentPageTaskView = getCurrentPageTaskView();
+        int currentPageTaskViewId;
+        if (currentPageTaskView != null) {
+            currentPageTaskViewId = currentPageTaskView.getTaskViewId();
+        } else if (mCurrentPage == indexOfChild(mClearAllButton)) {
+            currentPageTaskViewId = TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
+        } else {
+            return INVALID_PAGE;
+        }
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+        return nextGridPage == TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
+                ? indexOfChild(mClearAllButton)
+                : indexOfChild(getTaskViewFromTaskViewId(nextGridPage));
+    }
+
     private void runDismissAnimation(PendingAnimation pendingAnim) {
         AnimatorPlaybackController controller = pendingAnim.createPlaybackController();
         controller.dispatchOnStart();
@@ -4119,12 +4160,16 @@
         if (event.getAction() == KeyEvent.ACTION_DOWN) {
             switch (event.getKeyCode()) {
                 case KeyEvent.KEYCODE_TAB:
-                    return snapToPageRelative(getTaskViewCount(), event.isShiftPressed() ? -1 : 1,
-                            event.isAltPressed() /* cycle */);
+                    return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
+                            DIRECTION_TAB);
                 case KeyEvent.KEYCODE_DPAD_RIGHT:
-                    return snapToPageRelative(getPageCount(), mIsRtl ? -1 : 1, false /* cycle */);
+                    return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
                 case KeyEvent.KEYCODE_DPAD_LEFT:
-                    return snapToPageRelative(getPageCount(), mIsRtl ? 1 : -1, false /* cycle */);
+                    return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
                 case KeyEvent.KEYCODE_DEL:
                 case KeyEvent.KEYCODE_FORWARD_DEL:
                     dismissCurrentTask();
diff --git a/quickstep/tests/src/com/android/quickstep/util/TaskGridNavHelperTest.java b/quickstep/tests/src/com/android/quickstep/util/TaskGridNavHelperTest.java
new file mode 100644
index 0000000..7ef4910
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/util/TaskGridNavHelperTest.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2023 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.quickstep.util;
+
+import static com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID;
+import static com.android.quickstep.util.TaskGridNavHelper.INVALID_FOCUSED_TASK_ID;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.launcher3.util.IntArray;
+
+import org.junit.Test;
+
+public class TaskGridNavHelperTest {
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 1;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 2, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 1;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 2, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 2;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 1, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 2;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 1, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 1;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 3, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 2;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 4, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 3;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 1, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 4;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 2, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 1;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 2;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 5;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 6;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 1, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 6, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_withFocused_onFocused_pressLeft_toTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int focusedTaskId = 99;
+        int currentPageTaskViewId = focusedTaskId;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 1, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_withFocused_onFocused_pressUp_stayOnFocused() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int focusedTaskId = 99;
+        int currentPageTaskViewId = focusedTaskId;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_withFocused_onFocused_pressDown_stayOnFocused() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int focusedTaskId = 99;
+        int currentPageTaskViewId = focusedTaskId;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_withFocused_onFocused_pressRight_cycleToClearAll() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int focusedTaskId = 99;
+        int currentPageTaskViewId = focusedTaskId;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", CLEAR_ALL_PLACEHOLDER_ID, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_withFocused_onClearAll_pressLeft_cycleToFocusedTask() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int focusedTaskId = 99;
+        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, focusedTaskId);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", focusedTaskId, nextGridPage);
+    }
+
+    @Test
+    public void longerTopRow_noFocused_atEndTopBeyondBottom_pressDown_stayTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 7;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_DOWN;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 7, nextGridPage);
+    }
+
+    @Test
+    public void longerTopRow_noFocused_atEndTopBeyondBottom_pressUp_stayTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 7;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_UP;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 7, nextGridPage);
+    }
+
+    @Test
+    public void longerTopRow_noFocused_atEndBottom_pressLeft_goToTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 6;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_LEFT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 7, nextGridPage);
+    }
+
+    @Test
+    public void longerTopRow_noFocused_atClearAll_pressRight_goToLonger() {
+        IntArray topIds = IntArray.wrap(1, 3, 5, 7);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 7, nextGridPage);
+    }
+
+    @Test
+    public void longerBottomRow_noFocused_atClearAll_pressRight_goToLonger() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6, 7);
+        int currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_RIGHT;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 7, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 1;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 2, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 2;
+        int delta = 1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 3, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 3;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 2, nextGridPage);
+    }
+
+    @Test
+    public void equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
+        IntArray topIds = IntArray.wrap(1, 3, 5);
+        IntArray bottomIds = IntArray.wrap(2, 4, 6);
+        int currentPageTaskViewId = 2;
+        int delta = -1;
+        @TaskGridNavHelper.TASK_NAV_DIRECTION int direction = TaskGridNavHelper.DIRECTION_TAB;
+        boolean cycle = true;
+        TaskGridNavHelper taskGridNavHelper =
+                new TaskGridNavHelper(topIds, bottomIds, INVALID_FOCUSED_TASK_ID);
+
+        int nextGridPage =
+                taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle);
+
+        assertEquals("Wrong next page returned.", 1, nextGridPage);
+    }
+}