Made ACTION_SCROLL_IN_DIRECTION work with spans when scrolling left

Test: Added to GridLayoutManagerTest.java

Bug: 268487724

Change-Id: Ia6a422bf061f51ff3bf17faf6030a1cc743cc520
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
index 626819f..6b90c48 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -67,7 +67,6 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -1355,24 +1354,69 @@
 
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public void performActionScrollInDirection_focusLeft_vertical_withAvailableTarget()
+    public void performActionScrollInDirection_focusLeft_vertical_scrollTargetOnTheSameRow()
             throws Throwable {
 
         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
         //  earlier android version.
 
         final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
-       /*
+        /*
         This generates the following grid:
         1   2   3
         4
         */
-        runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_LEFT,
-                new HashMap<Integer, String>() {{
-                    put(1, "Item (1)");
-                    put(2, "Item (2)");
-                    put(3, "Item (3)");
-                }});
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(1));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (1)",
+                Pair.create(0, 0));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void performActionScrollInDirection_focusLeft_vertical_scrollTargetOnAPreviousRow()
+            throws Throwable {
+        // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+        //  earlier android version.
+
+        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
+        /*
+        This generates the following grid:
+        1   2   3
+        4   5
+        */
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(3));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (3)",
+                Pair.create(0, 2));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void performActionScrollInDirection_focusLeft_vertical_traversingThroughASpan()
+            throws Throwable {
+
+        // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+        //  earlier android version.
+
+        final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+        setUpRecyclerViewAndGridLayoutManager(4, VERTICAL);
+        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                if (position == 1) {
+                    return 2;
+                }
+                return 1;
+            }
+        });
+        waitForFirstLayout(mRecyclerView);
+        /*
+        This generates the following grid:
+        1   2   2
+        3   4
+        */
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (2)",
+                Pair.create(0, 1));
     }
 
     @Test
@@ -1382,36 +1426,101 @@
         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
         //  earlier android version.
 
-        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, VERTICAL);
+        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, VERTICAL);
         /*
         This generates the following grid:
         1   2   3
-        4
+        4   5
         */
-        runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_LEFT,
-                Collections.singletonList(0));
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
+        runScrollInDirectionAndFail(View.FOCUS_LEFT, Pair.create(0, 0));
     }
 
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public void performActionScrollInDirection_focusLeft_horizontal_withAvailableTarget()
+    public void performActionScrollInDirection_focusLeft_horizontal_scrollTargetOnTheSameRow()
             throws Throwable {
         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
         //  earlier android versions.
 
-        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
         /*
         This generates the following grid:
         1   4
-        2
+        2   5
         3
         */
-        runScrollInDirectionOnMultipleItemsAndSucceed(uiAutomation, View.FOCUS_LEFT,
-                new HashMap<Integer, String>() {{
-                    put(1, "Item (4)");
-                    put(2, "Item (2)");
-                    put(3, "Item (1)");
-                }});
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (2)" ,
+                Pair.create(1, 0));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void performActionScrollInDirection_focusLeft_horizontal_traversingThroughASpan()
+            throws Throwable {
+        // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+        //  earlier android versions.
+
+        final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+        setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
+        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                if (position == 4) {
+                    return 2;
+                }
+                return 1;
+            }
+        });
+        waitForFirstLayout(mRecyclerView);
+        /*
+        This generates the following grid:
+        1   4   6
+        2   5   7
+        3   5   8
+        */
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(7));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (5)" ,
+                Pair.create(2, 1));
+
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(4));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (3)" ,
+                Pair.create(2, 0));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void performActionScrollInDirection_focusLeft_horizontal_withWrapAround()
+            throws Throwable {
+        // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
+        //  earlier android versions.
+
+        final UiAutomation uiAutomation = setUpAndReturnUiAutomation();
+        setUpRecyclerViewAndGridLayoutManager(8, HORIZONTAL);
+        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                if (position == 6) {
+                    return 2;
+                }
+                return 1;
+            }
+        });
+        waitForFirstLayout(mRecyclerView);
+        /*
+        This generates the following grid:
+        1   4   7
+        2   5   7
+        3   6   8
+        */
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(2));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (7)" ,
+                Pair.create(1, 2));
+
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(6));
+        runScrollInDirectionAndSucceed(uiAutomation, View.FOCUS_LEFT, "Item (5)" ,
+                Pair.create(1, 1));
     }
 
     @Test
@@ -1421,15 +1530,15 @@
         // TODO(b/267511848): suppress to LOLLIPOP once U constants are finalized and available in
         //  earlier android versions.
 
-        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(4, HORIZONTAL);
+        final UiAutomation uiAutomation = setUpGridLayoutManagerAccessibilityTest(5, HORIZONTAL);
         /*
         This generates the following grid:
         1   4
-        2
+        2   5
         3
         */
-        runScrollInDirectionOnMultipleItemsAndFail(uiAutomation, View.FOCUS_LEFT,
-                Collections.singletonList(0));
+        setAccessibilityFocus(uiAutomation, mGlm.getChildAt(0));
+        runScrollInDirectionAndFail(View.FOCUS_LEFT, Pair.create(0, 0));
     }
 
     @Test
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index 5ab22e0..a5df2db 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -296,16 +296,17 @@
 
             int scrollTargetPosition;
 
+            int row = (mRowWithAccessibilityFocus == INVALID_POSITION) ? startingRow
+                    : mRowWithAccessibilityFocus;
+            int column = (mColumnWithAccessibilityFocus == INVALID_POSITION)
+                    ? startingColumn : mColumnWithAccessibilityFocus;
+
             switch (direction) {
                 case View.FOCUS_LEFT:
-                    scrollTargetPosition = findScrollTargetPositionOnTheLeft(startingRow,
-                            startingColumn, startingAdapterPosition);
+                    scrollTargetPosition = findScrollTargetPositionOnTheLeft(row, column,
+                            startingAdapterPosition);
                     break;
                 case View.FOCUS_RIGHT:
-                    int row = (mRowWithAccessibilityFocus == INVALID_POSITION) ? startingRow
-                            : mRowWithAccessibilityFocus;
-                    int column = (mColumnWithAccessibilityFocus == INVALID_POSITION)
-                            ? startingColumn : mColumnWithAccessibilityFocus;
                     scrollTargetPosition =
                             findScrollTargetPositionOnTheRight(row, column,
                                     startingAdapterPosition);
@@ -461,25 +462,37 @@
                 return INVALID_POSITION;
             }
 
-            // Canonical case: target is on the same row. TODO (b/268487724): handle RTL.
-            if (currentRow == startingRow && currentColumn < startingColumn) {
-                return i;
-            } else {
-                if (mOrientation == VERTICAL) {
-                    /*
-                     * Grids with vertical layouts are laid out row by row...
-                     * 1   2   3
-                     * 4   5   6
-                     * 7   8
-                     * ... and the scroll target may lie on a preceding row.
-                     */
-                    if (currentRow < startingRow) {
-                        scrollTargetPosition = i;
-                        break;
-                    }
-                } else { // HORIZONTAL
-                    // TODO (b/268487724): handle case where the scroll target spans multiple
-                    //  rows/columns.
+            if (mOrientation == VERTICAL) {
+                /*
+                 * For grids with vertical orientation...
+                 * 1   2   3
+                 * 4   5   5
+                 * 6   7
+                 * ... the scroll target may lie on the same or a preceding row.
+                 */
+                // TODO (b/268487724): handle RTL.
+                if ((currentRow == startingRow && currentColumn < startingColumn)
+                        || (currentRow < startingRow)) {
+                    scrollTargetPosition = i;
+                    mRowWithAccessibilityFocus = currentRow;
+                    mColumnWithAccessibilityFocus = currentColumn;
+                    break;
+                }
+            } else { // HORIZONTAL
+                /*
+                 * For grids with horizontal orientation, the scroll target may span multiple
+                 * rows. For example, in this grid...
+                 * 1   4   6
+                 * 2   5   7
+                 * 3   5   8
+                 * ... moving from 8 to 5 or from 7 to 5 is considered staying on the "same row"
+                 * because the row indices for 5 include 8's and 7's row.
+                 */
+                if (getRowIndices(i).contains(startingRow) && currentColumn < startingColumn) {
+                    // Note: mRowWithAccessibilityFocus not updated since the scroll target is on
+                    // the same row.
+                    mColumnWithAccessibilityFocus = currentColumn;
+                    return i;
                 }
             }
         }
@@ -564,22 +577,35 @@
         // ... the generated map - {2 -> 5, 1 -> 7, 0 -> 6} - can be used to scroll from,
         // say, "2" (adapter position 1) in the second row to "7" (adapter position 6) in the
         // preceding row.
+        //
+        // Sometimes cells span multiple rows. In this example:
+        // 1   4   7
+        // 2   5   7
+        // 3   6   8
+        // ... the generated map - {0 -> 6, 1 -> 6, 2 -> 7} - can be used to scroll left from,
+        // say, "3" (adapter position 2) in the third row to "7" (adapter position 6) on the
+        // second row, and then to "5" (adapter position 4).
         Map<Integer, Integer> rowToLastItemPositionMap = new TreeMap<>(Collections.reverseOrder());
         for (int position = 0; position < getItemCount(); position++) {
-            int row = getRowIndex(position);
-            if (row < 0) {
-                if (DEBUG) {
-                    throw new RuntimeException(
-                            "row equals " + row + ". It cannot be less than zero");
+            Set<Integer> rows = getRowIndices(position);
+            for (int row: rows) {
+                if (row < 0) {
+                    if (DEBUG) {
+                        throw new RuntimeException(
+                                "row equals " + row + ". It cannot be less than zero");
+                    }
+                    return INVALID_POSITION;
                 }
-                return INVALID_POSITION;
+                rowToLastItemPositionMap.put(row, position);
             }
-            rowToLastItemPositionMap.put(row, position);
         }
 
         for (int row : rowToLastItemPositionMap.keySet()) {
             if (row < startingRow) {
-                return rowToLastItemPositionMap.get(row);
+                int scrollTargetPosition = rowToLastItemPositionMap.get(row);
+                mRowWithAccessibilityFocus = row;
+                mColumnWithAccessibilityFocus = getColumnIndex(scrollTargetPosition);
+                return scrollTargetPosition;
             }
         }
         return INVALID_POSITION;