Make GridLayoutManager public and settable and BaseGridView.

Test: Added unit tests for switching/restoring GridLayoutManager on
BaseGridView.
Relnote: "Made Leanback GridLayoutManager public to allow BaseGridView's
layout manager to be manually set."

Change-Id: Ie0abc3dcf6ad2202056b4ed4a6e7cb9279efe1a8
diff --git a/leanback/leanback/api/api_lint.ignore b/leanback/leanback/api/api_lint.ignore
index c4a34f6..67688b7 100644
--- a/leanback/leanback/api/api_lint.ignore
+++ b/leanback/leanback/api/api_lint.ignore
@@ -87,6 +87,8 @@
     Class should be named DataCallback
 
 
+ConcreteCollection: androidx.leanback.widget.GridLayoutManager#onAddFocusables(androidx.recyclerview.widget.RecyclerView, java.util.ArrayList<android.view.View>, int, int) parameter #1:
+    Parameter type is concrete collection (`java.util.ArrayList`); must be higher-level interface
 ConcreteCollection: androidx.leanback.widget.ItemBridgeAdapter#getPresenterMapper():
     Return type is concrete collection (`java.util.ArrayList`); must be higher-level interface
 ConcreteCollection: androidx.leanback.widget.ItemBridgeAdapter#setPresenterMapper(java.util.ArrayList<androidx.leanback.widget.Presenter>) parameter #0:
diff --git a/leanback/leanback/api/current.txt b/leanback/leanback/api/current.txt
index d92b77a..de3d795 100644
--- a/leanback/leanback/api/current.txt
+++ b/leanback/leanback/api/current.txt
@@ -1891,6 +1891,12 @@
     method public void startPostponedEnterTransition();
   }
 
+  public final class GridLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager {
+    ctor public GridLayoutManager();
+    method public androidx.recyclerview.widget.RecyclerView.LayoutParams generateDefaultLayoutParams();
+    method public void setOrientation(int);
+  }
+
   public class GuidanceStylist implements androidx.leanback.widget.FragmentAnimationProvider {
     ctor public GuidanceStylist();
     method public android.widget.TextView! getBreadcrumbView();
diff --git a/leanback/leanback/api/public_plus_experimental_current.txt b/leanback/leanback/api/public_plus_experimental_current.txt
index d92b77a..e046040 100644
--- a/leanback/leanback/api/public_plus_experimental_current.txt
+++ b/leanback/leanback/api/public_plus_experimental_current.txt
@@ -1891,6 +1891,12 @@
     method public void startPostponedEnterTransition();
   }
 
+  public final class GridLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager {
+    ctor public GridLayoutManager();
+    method public androidx.recyclerview.widget.RecyclerView.LayoutParams generateDefaultLayoutParams();
+    method public void setOrientation(@androidx.recyclerview.widget.RecyclerView.Orientation int);
+  }
+
   public class GuidanceStylist implements androidx.leanback.widget.FragmentAnimationProvider {
     ctor public GuidanceStylist();
     method public android.widget.TextView! getBreadcrumbView();
diff --git a/leanback/leanback/api/restricted_current.txt b/leanback/leanback/api/restricted_current.txt
index 65d5941..5ca4b08 100644
--- a/leanback/leanback/api/restricted_current.txt
+++ b/leanback/leanback/api/restricted_current.txt
@@ -2064,6 +2064,12 @@
     method public void startPostponedEnterTransition();
   }
 
+  public final class GridLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager {
+    ctor public GridLayoutManager();
+    method public androidx.recyclerview.widget.RecyclerView.LayoutParams generateDefaultLayoutParams();
+    method public void setOrientation(@androidx.recyclerview.widget.RecyclerView.Orientation int);
+  }
+
   public class GuidanceStylist implements androidx.leanback.widget.FragmentAnimationProvider {
     ctor public GuidanceStylist();
     method public android.widget.TextView! getBreadcrumbView();
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
index 5f7a59a..b6bae78 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/GridWidgetTest.java
@@ -715,9 +715,123 @@
 
     }
 
+    @Test(expected = ClassCastException.class)
+    public void testSetInvalidLayoutManager() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid);
+        initActivity(intent);
+
+        // BaseGridView only accepts Leanback's GridLayoutManager for its layout manager.
+        RecyclerView.LayoutManager baseLayoutManager =
+                Mockito.mock(RecyclerView.LayoutManager.class);
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.mGridView.setLayoutManager(baseLayoutManager);
+            }
+        });
+    }
+
+    @Test
+    public void testSwitchLayoutManagerVertical() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+        initActivity(intent);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 3;
+
+        scrollToEnd(mVerifyLayout);
+
+        GridLayoutManager newGridLayoutManager = new GridLayoutManager();
+        newGridLayoutManager.setOrientation(RecyclerView.VERTICAL);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.setLayoutManager(newGridLayoutManager);
+            }
+        });
+        mNumRows = 1;
+        waitOneUiCycle();
+
+        // Resetting the layout manager should bring focus back to the first element.
+        verifyBeginAligned();
+        scrollToEnd(mVerifyLayout);
+        scrollToBegin(mVerifyLayout);
+        verifyBeginAligned();
+    }
+
+    @Test
+    public void testSwitchLayoutManagerHorizontal() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.horizontal_grid);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+        initActivity(intent);
+        mOrientation = BaseGridView.HORIZONTAL;
+        mNumRows = 3;
+
+        scrollToEnd(mVerifyLayout);
+
+        GridLayoutManager newGridLayoutManager = new GridLayoutManager();
+        newGridLayoutManager.setOrientation(RecyclerView.HORIZONTAL);
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.setLayoutManager(newGridLayoutManager);
+                ((HorizontalGridView) mGridView).setNumRows(4);
+            }
+        });
+        mNumRows = 4;
+        waitOneUiCycle();
+
+        verifyBeginAligned();
+        scrollToEnd(mVerifyLayout);
+        scrollToBegin(mVerifyLayout);
+        verifyBeginAligned();
+    }
+
+    @Test
+    public void testRestoreLayoutManager() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid);
+        intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
+        initActivity(intent);
+        mOrientation = BaseGridView.VERTICAL;
+        mNumRows = 3;
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mGridView.scrollToPosition(29);
+            }
+        });
+
+        GridLayoutManager layout = (GridLayoutManager) mGridView.getLayoutManager();
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.mGridView.setLayoutManager(null);
+            }
+        });
+
+        waitOneUiCycle();
+
+        mActivityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mActivity.mGridView.setLayoutManager(layout);
+            }
+        });
+
+        waitOneUiCycle();
+
+        assertEquals(29, mGridView.getSelectedPosition());
+    }
+
     @Test
     public void testThreeColumnVerticalBasic() throws Throwable {
-
         Intent intent = new Intent();
         intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID, R.layout.vertical_grid);
         intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 200);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/BaseGridView.java b/leanback/leanback/src/main/java/androidx/leanback/widget/BaseGridView.java
index 82864ee..5b7aaf7 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/BaseGridView.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/BaseGridView.java
@@ -233,7 +233,7 @@
         void onLayoutCompleted(@NonNull RecyclerView.State state);
     }
 
-    final GridLayoutManager mLayoutManager;
+    GridLayoutManager mLayoutManager;
 
     private SmoothScrollByBehavior mSmoothScrollByBehavior;
 
@@ -897,6 +897,22 @@
     }
 
     @Override
+    public void setLayoutManager(@Nullable RecyclerView.LayoutManager layout) {
+        if (layout == null) {
+            super.setLayoutManager(null);
+            if (mLayoutManager != null) {
+                mLayoutManager.setGridView(null);
+            }
+            mLayoutManager = null;
+            return;
+        }
+
+        mLayoutManager = (GridLayoutManager) layout;
+        mLayoutManager.setGridView(this);
+        super.setLayoutManager(layout);
+    }
+
+    @Override
     public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
         if ((mPrivateFlag & PFLAG_RETAIN_FOCUS_FOR_CHILD) == PFLAG_RETAIN_FOCUS_FOR_CHILD) {
             // dont focus to child if GridView itself retains focus for child
@@ -1152,7 +1168,9 @@
      */
     @Override
     public void onRtlPropertiesChanged(int layoutDirection) {
-        mLayoutManager.onRtlPropertiesChanged(layoutDirection);
+        if (mLayoutManager != null) {
+            mLayoutManager.onRtlPropertiesChanged(layoutDirection);
+        }
     }
 
     /**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
index e71696c..c4bfc64 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/widget/GridLayoutManager.java
@@ -59,7 +59,11 @@
 import java.util.Arrays;
 import java.util.List;
 
-final class GridLayoutManager extends RecyclerView.LayoutManager {
+/**
+ * A {@link RecyclerView.LayoutManager} implementation that lays out items in a grid for leanback
+ * {@link VerticalGridView} and {@link HorizontalGridView}.
+ */
+public final class GridLayoutManager extends RecyclerView.LayoutManager {
 
     /*
      * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
@@ -408,7 +412,7 @@
         return TAG + ":" + mBaseGridView.getId();
     }
 
-    final BaseGridView mBaseGridView;
+    BaseGridView mBaseGridView;
 
     /**
      * Note on conventions in the presence of RTL layout directions:
@@ -715,13 +719,22 @@
      */
     private FacetProviderAdapter mFacetProviderAdapter;
 
-    GridLayoutManager(@NonNull BaseGridView baseGridView) {
+    public GridLayoutManager() {
+        this(null);
+    }
+
+    GridLayoutManager(@Nullable BaseGridView baseGridView) {
         mBaseGridView = baseGridView;
         mChildVisibility = -1;
         // disable prefetch by default, prefetch causes regression on low power chipset
         setItemPrefetchEnabled(false);
     }
 
+    void setGridView(BaseGridView baseGridView) {
+        mBaseGridView = baseGridView;
+        mGrid = null;
+    }
+
     public void setOrientation(@RecyclerView.Orientation int orientation) {
         if (orientation != HORIZONTAL && orientation != VERTICAL) {
             if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
@@ -1064,6 +1077,11 @@
     }
 
     @Override
+    public boolean checkLayoutParams(@Nullable RecyclerView.LayoutParams lp) {
+        return lp instanceof LayoutParams;
+    }
+
+    @Override
     public boolean canScrollHorizontally() {
         // We can scroll horizontally if we have horizontal orientation, or if
         // we are vertical and have more than one column.
@@ -2898,8 +2916,8 @@
     }
 
     @Override
-    public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull View child,
-            @NonNull View focused) {
+    public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state,
+            @NonNull View child, @Nullable View focused) {
         if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
             return true;
         }