Convert TaskGridNavHelper to Kotlin.

Fix: 396114184
Test: TaskGridNavHelperTest
Flag: EXEMPT Kotlin conversion
Change-Id: I1c3da7eac7f41a0763293a0b408f608799f02cf2
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
deleted file mode 100644
index 35e90f2..0000000
--- a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * 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;
-import java.util.List;
-
-/**
- * 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 ADD_DESK_PLACEHOLDER_ID = -2;
-
-    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 final IntArray mTopRowIds = new IntArray();
-    private final IntArray mBottomRowIds = new IntArray();
-
-    public TaskGridNavHelper(IntArray topIds, IntArray bottomIds,
-            List<Integer> largeTileIds, boolean hasAddDesktopButton) {
-        mOriginalTopRowIds = topIds.clone();
-        generateTaskViewIdGrid(topIds, bottomIds, largeTileIds, hasAddDesktopButton);
-    }
-
-    private void generateTaskViewIdGrid(IntArray topRowIdArray, IntArray bottomRowIdArray,
-            List<Integer> largeTileIds, boolean hasAddDesktopButton) {
-        // Add AddDesktopButton and lage tiles to both rows.
-        if (hasAddDesktopButton) {
-            mTopRowIds.add(ADD_DESK_PLACEHOLDER_ID);
-            mBottomRowIds.add(ADD_DESK_PLACEHOLDER_ID);
-        }
-        for (Integer tileId : largeTileIds) {
-            mTopRowIds.add(tileId);
-            mBottomRowIds.add(tileId);
-        }
-
-        // Add row ids to their respective rows.
-        mTopRowIds.addAll(topRowIdArray);
-        mBottomRowIds.addAll(bottomRowIdArray);
-
-        // Fill in the shorter array with the ids from the longer one.
-        while (mTopRowIds.size() > mBottomRowIds.size()) {
-            mBottomRowIds.add(mTopRowIds.get(mBottomRowIds.size()));
-        }
-        while (mBottomRowIds.size() > mTopRowIds.size()) {
-            mTopRowIds.add(mBottomRowIds.get(mTopRowIds.size()));
-        }
-
-        // Add the clear all button to the end of both arrays.
-        mTopRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
-        mBottomRowIds.add(CLEAR_ALL_PLACEHOLDER_ID);
-    }
-
-    /**
-     * 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)) {
-                        if (boundedIndex < 0) {
-                            // If no cycling, always return the first task.
-                            return mTopRowIds.get(0);
-                        } else {
-                            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;
-        }
-    }
-
-    /**
-     * Returns the column of a task's id in the grid.
-     */
-    public int getColumn(int taskViewId) {
-        return mTopRowIds.contains(taskViewId) ? mTopRowIds.indexOf(taskViewId)
-                : mBottomRowIds.indexOf(taskViewId);
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
new file mode 100644
index 0000000..0e78801
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskGridNavHelper.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2025 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 com.android.launcher3.util.IntArray
+import kotlin.math.abs
+import kotlin.math.max
+
+/** Helper class for navigating RecentsView grid tasks via arrow keys and tab. */
+class TaskGridNavHelper(
+    private val topIds: IntArray,
+    bottomIds: IntArray,
+    largeTileIds: List<Int>,
+    hasAddDesktopButton: Boolean,
+) {
+    private val topRowIds = mutableListOf<Int>()
+    private val bottomRowIds = mutableListOf<Int>()
+
+    init {
+        // Add AddDesktopButton and lage tiles to both rows.
+        if (hasAddDesktopButton) {
+            topRowIds += ADD_DESK_PLACEHOLDER_ID
+            bottomRowIds += ADD_DESK_PLACEHOLDER_ID
+        }
+        topRowIds += largeTileIds
+        bottomRowIds += largeTileIds
+
+        // Add row ids to their respective rows.
+        topRowIds += topIds
+        bottomRowIds += bottomIds
+
+        // Fill in the shorter array with the ids from the longer one.
+        topRowIds += bottomRowIds.takeLast(max(bottomRowIds.size - topRowIds.size, 0))
+        bottomRowIds += topRowIds.takeLast(max(topRowIds.size - bottomRowIds.size, 0))
+
+        // Add the clear all button to the end of both arrays.
+        topRowIds += CLEAR_ALL_PLACEHOLDER_ID
+        bottomRowIds += CLEAR_ALL_PLACEHOLDER_ID
+    }
+
+    /** Returns the id of the next page in the grid or -1 for the clear all button. */
+    fun getNextGridPage(
+        currentPageTaskViewId: Int,
+        delta: Int,
+        direction: TaskNavDirection,
+        cycle: Boolean,
+    ): Int {
+        val inTop = topRowIds.contains(currentPageTaskViewId)
+        val index =
+            if (inTop) topRowIds.indexOf(currentPageTaskViewId)
+            else bottomRowIds.indexOf(currentPageTaskViewId)
+        val maxSize = max(topRowIds.size, bottomRowIds.size)
+        val nextIndex = index + delta
+
+        return when (direction) {
+            TaskNavDirection.UP,
+            TaskNavDirection.DOWN -> {
+                if (inTop) bottomRowIds[index] else topRowIds[index]
+            }
+            TaskNavDirection.LEFT -> {
+                val boundedIndex =
+                    if (cycle) nextIndex % maxSize else nextIndex.coerceAtMost(maxSize - 1)
+                if (inTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+            }
+            TaskNavDirection.RIGHT -> {
+                val boundedIndex =
+                    if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex)
+                    else nextIndex.coerceAtLeast(0)
+                val inOriginalTop = topIds.contains(currentPageTaskViewId)
+                if (inOriginalTop) topRowIds[boundedIndex] else bottomRowIds[boundedIndex]
+            }
+            TaskNavDirection.TAB -> {
+                val boundedIndex =
+                    if (cycle) (if (nextIndex < 0) maxSize - 1 else nextIndex % maxSize)
+                    else nextIndex.coerceAtMost(maxSize - 1)
+                if (delta >= 0) {
+                    if (inTop && topRowIds[index] != bottomRowIds[index]) bottomRowIds[index]
+                    else topRowIds[boundedIndex]
+                } else {
+                    if (topRowIds.contains(currentPageTaskViewId)) {
+                        if (boundedIndex < 0) {
+                            // If no cycling, always return the first task.
+                            topRowIds[0]
+                        } else {
+                            bottomRowIds[boundedIndex]
+                        }
+                    } else {
+                        // Go up to top if there is task above
+                        if (topRowIds[index] != bottomRowIds[index]) topRowIds[index]
+                        else bottomRowIds[boundedIndex]
+                    }
+                }
+            }
+            else -> currentPageTaskViewId
+        }
+    }
+
+    /**
+     * Returns a sequence of pairs of (TaskView ID, offset) in the grid, ordered according to tab
+     * navigation, starting from the initial TaskView ID, towards the start or end of the grid.
+     *
+     * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
+     * negative value moves backward towards the beginning. The offset is the distance between
+     * columns the tasks are in.
+     */
+    fun gridTaskViewIdOffsetPairInTabOrderSequence(
+        initialTaskViewId: Int,
+        towardsStart: Boolean,
+    ): Sequence<Pair<Int, Int>> = sequence {
+        val draggedTaskViewColumn = getColumn(initialTaskViewId)
+        var nextTaskViewId: Int = initialTaskViewId
+        var previousTaskViewId: Int = Int.MIN_VALUE
+        while (nextTaskViewId != previousTaskViewId && nextTaskViewId >= 0) {
+            previousTaskViewId = nextTaskViewId
+            nextTaskViewId =
+                getNextGridPage(
+                    nextTaskViewId,
+                    if (towardsStart) -1 else 1,
+                    TaskNavDirection.TAB,
+                    cycle = false,
+                )
+            if (nextTaskViewId >= 0 && nextTaskViewId != previousTaskViewId) {
+                val columnOffset = abs(getColumn(nextTaskViewId) - draggedTaskViewColumn)
+                yield(Pair(nextTaskViewId, columnOffset))
+            }
+        }
+    }
+
+    /** Returns the column of a task's id in the grid. */
+    private fun getColumn(taskViewId: Int): Int =
+        if (topRowIds.contains(taskViewId)) topRowIds.indexOf(taskViewId)
+        else bottomRowIds.indexOf(taskViewId)
+
+    enum class TaskNavDirection {
+        UP,
+        DOWN,
+        LEFT,
+        RIGHT,
+        TAB,
+    }
+
+    companion object {
+        const val CLEAR_ALL_PLACEHOLDER_ID: Int = -1
+        const val ADD_DESK_PLACEHOLDER_ID: Int = -2
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 424271a..e4633e4 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -65,11 +65,6 @@
 import static com.android.quickstep.BaseContainerInterface.getTaskDimension;
 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.OverviewActionsView.HIDDEN_ACTIONS_IN_MENU;
 import static com.android.quickstep.views.OverviewActionsView.HIDDEN_DESKTOP;
@@ -4540,7 +4535,7 @@
     }
 
     private boolean snapToPageRelative(int delta, boolean cycle,
-            @TaskGridNavHelper.TASK_NAV_DIRECTION int direction) {
+            TaskGridNavHelper.TaskNavDirection direction) {
         // Set next page if scroll animation is still running, otherwise cannot snap to the
         // next page on successive key presses. Setting the current page aborts the scroll.
         if (!mScroller.isFinished()) {
@@ -4559,7 +4554,7 @@
         return true;
     }
 
-    private int getNextPageInternal(int delta, @TaskGridNavHelper.TASK_NAV_DIRECTION int direction,
+    private int getNextPageInternal(int delta, TaskGridNavHelper.TaskNavDirection direction,
             boolean cycle) {
         if (!showAsGrid()) {
             return getNextPage() + delta;
@@ -4647,15 +4642,19 @@
         switch (event.getKeyCode()) {
             case KeyEvent.KEYCODE_TAB:
                 return snapToPageRelative(event.isShiftPressed() ? -1 : 1, true /* cycle */,
-                        DIRECTION_TAB);
+                        TaskGridNavHelper.TaskNavDirection.TAB);
             case KeyEvent.KEYCODE_DPAD_RIGHT:
-                return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */, DIRECTION_RIGHT);
+                return snapToPageRelative(mIsRtl ? -1 : 1, true /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.RIGHT);
             case KeyEvent.KEYCODE_DPAD_LEFT:
-                return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */, DIRECTION_LEFT);
+                return snapToPageRelative(mIsRtl ? 1 : -1, true /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.LEFT);
             case KeyEvent.KEYCODE_DPAD_UP:
-                return snapToPageRelative(1, false /* cycle */, DIRECTION_UP);
+                return snapToPageRelative(1, false /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.UP);
             case KeyEvent.KEYCODE_DPAD_DOWN:
-                return snapToPageRelative(1, false /* cycle */, DIRECTION_DOWN);
+                return snapToPageRelative(1, false /* cycle */,
+                        TaskGridNavHelper.TaskNavDirection.DOWN);
             case KeyEvent.KEYCODE_DEL:
             case KeyEvent.KEYCODE_FORWARD_DEL:
                 dismissCurrentTask();
diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
index d18a5f7..d3549fb 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
+++ b/quickstep/src/com/android/quickstep/views/RecentsViewUtils.kt
@@ -432,7 +432,23 @@
         towardsStart: Boolean,
     ): Sequence<Pair<TaskView, Int>> {
         if (recentsView.showAsGrid()) {
-            return gridTaskOffsetPairInTabOrderSequence(draggedTaskView, towardsStart)
+            val taskGridNavHelper =
+                TaskGridNavHelper(
+                    recentsView.topRowIdArray,
+                    recentsView.bottomRowIdArray,
+                    getLargeTaskViewIds(),
+                    hasAddDesktopButton = false,
+                )
+            return taskGridNavHelper
+                .gridTaskViewIdOffsetPairInTabOrderSequence(
+                    draggedTaskView.taskViewId,
+                    towardsStart,
+                )
+                .mapNotNull { (taskViewId, columnOffset) ->
+                    recentsView.getTaskViewFromTaskViewId(taskViewId)?.let { taskView ->
+                        Pair(taskView, columnOffset)
+                    }
+                }
         } else {
             val taskViewList = taskViews.toList()
             val draggedTaskViewIndex = taskViewList.indexOf(draggedTaskView)
@@ -452,49 +468,6 @@
         }
     }
 
-    /**
-     * Returns a sequence of pairs of (TaskViews, offsets) in the grid, ordered according to tab
-     * navigation, starting from the dragged TaskView, towards the start or end of the grid.
-     *
-     * <p>A positive delta moves forward in the tab order towards the end of the grid, while a
-     * negative value moves backward towards the beginning. The offset is the distance between
-     * columns the tasks are in.
-     */
-    private fun gridTaskOffsetPairInTabOrderSequence(
-        draggedTaskView: TaskView,
-        towardsStart: Boolean,
-    ): Sequence<Pair<TaskView, Int>> = sequence {
-        val taskGridNavHelper =
-            TaskGridNavHelper(
-                recentsView.topRowIdArray,
-                recentsView.bottomRowIdArray,
-                getLargeTaskViewIds(),
-                /* hasAddDesktopButton= */ false,
-            )
-        val draggedTaskViewColumn = taskGridNavHelper.getColumn(draggedTaskView.taskViewId)
-        var nextTaskView: TaskView? = draggedTaskView
-        var previousTaskView: TaskView? = null
-        while (nextTaskView != previousTaskView && nextTaskView != null) {
-            previousTaskView = nextTaskView
-            nextTaskView =
-                recentsView.getTaskViewFromTaskViewId(
-                    taskGridNavHelper.getNextGridPage(
-                        nextTaskView.taskViewId,
-                        if (towardsStart) -1 else 1,
-                        TaskGridNavHelper.DIRECTION_TAB,
-                        /* cycle = */ false,
-                    )
-                )
-            if (nextTaskView != null && nextTaskView != previousTaskView) {
-                val columnOffset =
-                    abs(
-                        taskGridNavHelper.getColumn(nextTaskView.taskViewId) - draggedTaskViewColumn
-                    )
-                yield(Pair(nextTaskView, columnOffset))
-            }
-        }
-    }
-
     /** Creates a neighboring task view spring, driven by the spring of its neighbor. */
     private fun createNeighboringTaskViewSpringAnimation(
         taskView: TaskView,
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
index f2fa0c5..cb088fd 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/TaskGridNavHelperTest.kt
@@ -16,13 +16,13 @@
 package com.android.quickstep.util
 
 import com.android.launcher3.util.IntArray
-import com.android.quickstep.util.TaskGridNavHelper.ADD_DESK_PLACEHOLDER_ID
-import com.android.quickstep.util.TaskGridNavHelper.CLEAR_ALL_PLACEHOLDER_ID
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_DOWN
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_LEFT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_RIGHT
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_TAB
-import com.android.quickstep.util.TaskGridNavHelper.DIRECTION_UP
+import com.android.quickstep.util.TaskGridNavHelper.Companion.ADD_DESK_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.Companion.CLEAR_ALL_PLACEHOLDER_ID
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.DOWN
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.LEFT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.RIGHT
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.TAB
+import com.android.quickstep.util.TaskGridNavHelper.TaskNavDirection.UP
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 
@@ -35,8 +35,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressDown_goesToBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_DOWN, delta = 1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, DOWN, delta = 1)).isEqualTo(2)
     }
 
     /*                      ↑----→
@@ -46,7 +45,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressUp_goesToBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_UP, delta = 1)).isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, UP, delta = 1)).isEqualTo(2)
     }
 
     /*                      ↓----↑
@@ -57,8 +56,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressDown_goesToTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_DOWN, delta = 1))
-            .isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, DOWN, delta = 1)).isEqualTo(1)
     }
 
     /*
@@ -68,7 +66,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressUp_goesToTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_UP, delta = 1)).isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, UP, delta = 1)).isEqualTo(1)
     }
 
     /*
@@ -78,8 +76,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressLeft_goesLeft() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_LEFT, delta = 1))
-            .isEqualTo(3)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, LEFT, delta = 1)).isEqualTo(3)
     }
 
     /*
@@ -89,8 +86,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressLeft_goesLeft() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_LEFT, delta = 1))
-            .isEqualTo(4)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, LEFT, delta = 1)).isEqualTo(4)
     }
 
     /*
@@ -100,8 +96,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_secondItem_pressRight_goesRight() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_RIGHT, delta = -1))
-            .isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 3, RIGHT, delta = -1)).isEqualTo(1)
     }
 
     /*
@@ -111,8 +106,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_secondItem_pressRight_goesRight() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 4, DIRECTION_RIGHT, delta = -1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 4, RIGHT, delta = -1)).isEqualTo(2)
     }
 
     /*
@@ -124,7 +118,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressRight_cycleToClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_RIGHT, delta = -1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, RIGHT, delta = -1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -137,7 +131,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressRight_cycleToClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_RIGHT, delta = -1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, RIGHT, delta = -1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -149,7 +143,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_lastItem_pressLeft_toClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 5, DIRECTION_LEFT, delta = 1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 5, LEFT, delta = 1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -161,7 +155,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_lastItem_pressLeft_toClearAll() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 6, DIRECTION_LEFT, delta = 1))
+        assertThat(getNextGridPage(currentPageTaskViewId = 6, LEFT, delta = 1))
             .isEqualTo(CLEAR_ALL_PLACEHOLDER_ID)
     }
 
@@ -176,11 +170,7 @@
     @Test
     fun equalLengthRows_noFocused_onClearAll_pressLeft_cycleToFirst() {
         assertThat(
-                getNextGridPage(
-                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
-                    delta = 1,
-                )
+                getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, LEFT, delta = 1)
             )
             .isEqualTo(1)
     }
@@ -194,11 +184,7 @@
     @Test
     fun equalLengthRows_noFocused_onClearAll_pressRight_toLastInBottom() {
         assertThat(
-                getNextGridPage(
-                    currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
-                    delta = -1,
-                )
+                getNextGridPage(currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID, RIGHT, delta = -1)
             )
             .isEqualTo(6)
     }
@@ -214,7 +200,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -233,7 +219,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_UP,
+                    UP,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -254,7 +240,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_DOWN,
+                    DOWN,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -275,7 +261,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -297,7 +283,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(FOCUSED_TASK_ID),
                 )
@@ -316,7 +302,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 7,
-                    DIRECTION_DOWN,
+                    DOWN,
                     delta = 1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -335,7 +321,7 @@
         assertThat(
                 getNextGridPage(
                     /* topIds = */ currentPageTaskViewId = 7,
-                    DIRECTION_UP,
+                    UP,
                     delta = 1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -353,7 +339,7 @@
         assertThat(
                 getNextGridPage(
                     /* topIds = */ currentPageTaskViewId = 6,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -372,7 +358,7 @@
         assertThat(
                 getNextGridPage(
                     /* topIds = */ currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     topIds = IntArray.wrap(1, 3, 5, 7),
                 )
@@ -391,7 +377,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     bottomIds = IntArray.wrap(2, 4, 6, 7),
                 )
@@ -406,8 +392,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressTab_goesToBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = 1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = 1)).isEqualTo(2)
     }
 
     /*
@@ -418,8 +403,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressTab_goesToNextTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = 1))
-            .isEqualTo(3)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = 1)).isEqualTo(3)
     }
 
     /*
@@ -431,8 +415,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressTabWithShift_goesToPreviousBottom() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 3, DIRECTION_TAB, delta = -1))
-            .isEqualTo(2)
+        assertThat(getNextGridPage(currentPageTaskViewId = 3, TAB, delta = -1)).isEqualTo(2)
     }
 
     /*
@@ -442,8 +425,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onBottom_pressTabWithShift_goesToTop() {
-        assertThat(getNextGridPage(currentPageTaskViewId = 2, DIRECTION_TAB, delta = -1))
-            .isEqualTo(1)
+        assertThat(getNextGridPage(currentPageTaskViewId = 2, TAB, delta = -1)).isEqualTo(1)
     }
 
     /*
@@ -453,9 +435,7 @@
     */
     @Test
     fun equalLengthRows_noFocused_onTop_pressTabWithShift_noCycle_staysOnTop() {
-        assertThat(
-                getNextGridPage(currentPageTaskViewId = 1, DIRECTION_TAB, delta = -1, cycle = false)
-            )
+        assertThat(getNextGridPage(currentPageTaskViewId = 1, TAB, delta = -1, cycle = false))
             .isEqualTo(1)
     }
 
@@ -469,7 +449,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_TAB,
+                    TAB,
                     delta = 1,
                     cycle = false,
                 )
@@ -487,7 +467,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -505,7 +485,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -525,7 +505,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -546,7 +526,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -565,7 +545,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 2,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -584,7 +564,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 1,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -603,7 +583,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_TAB,
+                    TAB,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -621,7 +601,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = FOCUSED_TASK_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     topIds = IntArray(),
                     bottomIds = IntArray.wrap(2),
@@ -643,7 +623,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_TAB,
+                    TAB,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID, FOCUSED_TASK_ID),
                 )
@@ -662,7 +642,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 1,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     hasAddDesktopButton = true,
                 )
@@ -681,7 +661,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = 2,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     hasAddDesktopButton = true,
                 )
@@ -701,7 +681,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     hasAddDesktopButton = true,
                 )
@@ -722,7 +702,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = CLEAR_ALL_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     hasAddDesktopButton = true,
                 )
@@ -741,7 +721,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_UP,
+                    UP,
                     delta = 1,
                     hasAddDesktopButton = true,
                 )
@@ -760,7 +740,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_DOWN,
+                    DOWN,
                     delta = 1,
                     hasAddDesktopButton = true,
                 )
@@ -778,7 +758,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = ADD_DESK_PLACEHOLDER_ID,
-                    DIRECTION_LEFT,
+                    LEFT,
                     delta = 1,
                     largeTileIds = listOf(DESKTOP_TASK_ID),
                     hasAddDesktopButton = true,
@@ -797,7 +777,7 @@
         assertThat(
                 getNextGridPage(
                     currentPageTaskViewId = DESKTOP_TASK_ID,
-                    DIRECTION_RIGHT,
+                    RIGHT,
                     delta = -1,
                     largeTileIds = listOf(DESKTOP_TASK_ID),
                     hasAddDesktopButton = true,
@@ -806,9 +786,43 @@
             .isEqualTo(ADD_DESK_PLACEHOLDER_ID)
     }
 
+    // Col offset:  0   1   2
+    //             -----------
+    // ID grid:     4   2   0  start
+    //         end [5]  3   1
+    @Test
+    fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsStart() {
+        val expected = listOf(Pair(4, 0), Pair(3, 1), Pair(2, 1), Pair(1, 2), Pair(0, 2))
+        assertThat(
+                gridTaskViewIdOffsetPairInTabOrderSequence(
+                        initialTaskViewId = 5,
+                        towardsStart = true,
+                    )
+                    .toList()
+            )
+            .isEqualTo(expected)
+    }
+
+    // Col offset:  2   1   0
+    //             -----------
+    // ID grid:     4   2  [0] start
+    //          end 5   3   1
+    @Test
+    fun gridTaskViewIdOffsetPairInTabOrderSequence_towardsEnd() {
+        val expected = listOf(Pair(1, 0), Pair(2, 1), Pair(3, 1), Pair(4, 2), Pair(5, 2))
+        assertThat(
+                gridTaskViewIdOffsetPairInTabOrderSequence(
+                        initialTaskViewId = 0,
+                        towardsStart = false,
+                    )
+                    .toList()
+            )
+            .isEqualTo(expected)
+    }
+
     private fun getNextGridPage(
         currentPageTaskViewId: Int,
-        direction: Int,
+        direction: TaskGridNavHelper.TaskNavDirection,
         delta: Int,
         topIds: IntArray = IntArray.wrap(1, 3, 5),
         bottomIds: IntArray = IntArray.wrap(2, 4, 6),
@@ -821,6 +835,22 @@
         return taskGridNavHelper.getNextGridPage(currentPageTaskViewId, delta, direction, cycle)
     }
 
+    private fun gridTaskViewIdOffsetPairInTabOrderSequence(
+        initialTaskViewId: Int,
+        towardsStart: Boolean,
+        topIds: IntArray = IntArray.wrap(0, 2, 4),
+        bottomIds: IntArray = IntArray.wrap(1, 3, 5),
+        largeTileIds: List<Int> = emptyList(),
+        hasAddDesktopButton: Boolean = false,
+    ): Sequence<Pair<Int, Int>> {
+        val taskGridNavHelper =
+            TaskGridNavHelper(topIds, bottomIds, largeTileIds, hasAddDesktopButton)
+        return taskGridNavHelper.gridTaskViewIdOffsetPairInTabOrderSequence(
+            initialTaskViewId,
+            towardsStart,
+        )
+    }
+
     private companion object {
         const val FOCUSED_TASK_ID = 99
         const val DESKTOP_TASK_ID = 100