/*
 * Copyright 2018 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 androidx.recyclerview.widget;

import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;

import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.LargeTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
import android.util.SparseIntArray;
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;

import org.hamcrest.CoreMatchers;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

@LargeTest
@RunWith(AndroidJUnit4.class)
public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {

    @Test
    public void focusSearchFailureUp() throws Throwable {
        focusSearchFailure(false);
    }

    @Test
    public void focusSearchFailureDown() throws Throwable {
        focusSearchFailure(true);
    }

    @Test
    public void scrollToBadOffset() throws Throwable {
        scrollToBadOffset(false);
    }

    @Test
    public void scrollToBadOffsetReverse() throws Throwable {
        scrollToBadOffset(true);
    }

    private void scrollToBadOffset(boolean reverseLayout) throws Throwable {
        final int w = 500;
        final int h = 1000;
        RecyclerView recyclerView = setupBasic(new Config(2, 100).reverseLayout(reverseLayout),
                new GridTestAdapter(100) {
                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
                        if (lp == null) {
                            lp = new ViewGroup.LayoutParams(w / 2, h / 2);
                            holder.itemView.setLayoutParams(lp);
                        } else {
                            lp.width = w / 2;
                            lp.height = h / 2;
                            holder.itemView.setLayoutParams(lp);
                        }
                    }
                });
        TestedFrameLayout.FullControlLayoutParams lp
                = new TestedFrameLayout.FullControlLayoutParams(w, h);
        recyclerView.setLayoutParams(lp);
        waitForFirstLayout(recyclerView);
        mGlm.expectLayout(1);
        scrollToPosition(11);
        mGlm.waitForLayout(2);
        // assert spans and position etc
        for (int i = 0; i < mGlm.getChildCount(); i++) {
            View child = mGlm.getChildAt(i);
            GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) child
                    .getLayoutParams();
            assertThat("span index for child at " + i + " with position " + params
                            .getViewAdapterPosition(),
                    params.getSpanIndex(), CoreMatchers.is(params.getViewAdapterPosition() % 2));
        }
        // assert spans and positions etc.
        int lastVisible = mGlm.findLastVisibleItemPosition();
        // this should be the scrolled child
        assertThat(lastVisible, CoreMatchers.is(11));
    }

    private void focusSearchFailure(boolean scrollDown) throws Throwable {
        final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown)
                , new GridTestAdapter(31, 1) {
                    RecyclerView mAttachedRv;

                    @Override
                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                            int viewType) {
                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
                        testViewHolder.itemView.setFocusable(true);
                        testViewHolder.itemView.setFocusableInTouchMode(true);
                        // Good to have colors for debugging
                        StateListDrawable stl = new StateListDrawable();
                        stl.addState(new int[]{android.R.attr.state_focused},
                                new ColorDrawable(Color.RED));
                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
                        //noinspection deprecation using this for kitkat tests
                        testViewHolder.itemView.setBackgroundDrawable(stl);
                        return testViewHolder;
                    }

                    @Override
                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
                        mAttachedRv = recyclerView;
                    }

                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / 3);
                    }
                });
        waitForFirstLayout(recyclerView);

        View viewToFocus = recyclerView.findViewHolderForAdapterPosition(1).itemView;
        assertTrue(requestFocus(viewToFocus, true));
        assertSame(viewToFocus, recyclerView.getFocusedChild());
        int pos = 1;
        View focusedView = viewToFocus;
        while (pos < 31) {
            focusSearch(focusedView, scrollDown ? View.FOCUS_DOWN : View.FOCUS_UP);
            waitForIdleScroll(recyclerView);
            focusedView = recyclerView.getFocusedChild();
            assertEquals(Math.min(pos + 3, mAdapter.getItemCount() - 1),
                    recyclerView.getChildViewHolder(focusedView).getAdapterPosition());
            pos += 3;
        }
    }

    /**
     * Tests that the GridLayoutManager retains the focused element after multiple measure
     * calls to the RecyclerView.  There was a bug where the focused view was lost when the soft
     * keyboard opened.  This test simulates the measure/layout events triggered by the opening
     * of the soft keyboard by making two calls to measure.  A simulation was done because using
     * the soft keyboard in the test caused many issues on API levels 15, 17 and 19.
     */
    @Test
    public void focusedChildStaysInViewWhenRecyclerViewShrinks() throws Throwable {

        // Arrange.

        final int spanCount = 3;
        final int itemCount = 100;

        final RecyclerView recyclerView = inflateWrappedRV();
        ViewGroup.LayoutParams lp = recyclerView.getLayoutParams();
        lp.height = WRAP_CONTENT;
        lp.width = MATCH_PARENT;

        Config config = new Config(spanCount, itemCount);
        mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
                config.mReverseLayout);
        recyclerView.setLayoutManager(mGlm);

        GridFocusableAdapter gridFocusableAdapter = new GridFocusableAdapter(itemCount);
        gridFocusableAdapter.assignSpanSizeLookup(mGlm);
        recyclerView.setAdapter(gridFocusableAdapter);

        mGlm.expectLayout(1);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                getActivity().getContainer().addView(recyclerView);
            }
        });
        mGlm.waitForLayout(3);

        int width = recyclerView.getWidth();
        int height = recyclerView.getHeight();
        final int widthMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
        final int fullHeightMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
        // "MinusOne" so that a measure call will appropriately trigger onMeasure after RecyclerView
        // was previously laid out with the full height version.
        final int fullHeightMinusOneMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(height - 1, View.MeasureSpec.AT_MOST);
        final int halfHeightMeasureSpec =
                View.MeasureSpec.makeMeasureSpec(height / 2, View.MeasureSpec.AT_MOST);

        // Act 1.

        // First focus on the last fully visible child located at span index #1.
        View toFocus = findLastFullyVisibleChild(recyclerView);
        int focusIndex = recyclerView.getChildAdapterPosition(toFocus);
        focusIndex = (focusIndex / spanCount) * spanCount + 1;
        toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;
        assertTrue(focusIndex >= 1 && focusIndex < itemCount);

        requestFocus(toFocus, false);

        mGlm.expectLayout(1);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
                recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
                recyclerView.layout(
                        0,
                        0,
                        recyclerView.getMeasuredWidth(),
                        recyclerView.getMeasuredHeight());
            }
        });
        mGlm.waitForLayout(3);

        // Assert 1.

        assertThat("Child at position " + focusIndex + " should be focused",
                toFocus.hasFocus(), is(true));
        assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
                isViewPartiallyInBound(recyclerView, toFocus));

        // Act 2.

        mGlm.expectLayout(1);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                recyclerView.measure(widthMeasureSpec, fullHeightMeasureSpec);
                recyclerView.layout(
                        0,
                        0,
                        recyclerView.getMeasuredWidth(),
                        recyclerView.getMeasuredHeight());
            }
        });
        mGlm.waitForLayout(3);

        // Assert 2.

        assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
                isViewPartiallyInBound(recyclerView, toFocus));

        // Act 3.

        // Now focus on the first fully visible EditText located at the last span index.
        toFocus = findFirstFullyVisibleChild(recyclerView);
        focusIndex = recyclerView.getChildAdapterPosition(toFocus);
        focusIndex = (focusIndex / spanCount) * spanCount + (spanCount - 1);
        toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex).itemView;

        requestFocus(toFocus, false);

        mGlm.expectLayout(1);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                recyclerView.measure(widthMeasureSpec, fullHeightMinusOneMeasureSpec);
                recyclerView.measure(widthMeasureSpec, halfHeightMeasureSpec);
                recyclerView.layout(
                        0,
                        0,
                        recyclerView.getMeasuredWidth(),
                        recyclerView.getMeasuredHeight());
            }
        });
        mGlm.waitForLayout(3);

        // Assert 3.

        assertTrue("Child view at adapter pos " + focusIndex + " should be fully visible.",
                isViewPartiallyInBound(recyclerView, toFocus));
    }

    @Test
    public void topUnfocusableViewsVisibility() throws Throwable {
        // The maximum number of rows that can be fully in-bounds of RV.
        final int visibleRowCount = 5;
        final int spanCount = 3;
        final int consecutiveFocusableRowsCount = 4;
        final int consecutiveUnFocusableRowsCount = 8;
        final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
                * spanCount;

        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
                        .reverseLayout(true),
                new GridTestAdapter(itemCount, 1) {
                    RecyclerView mAttachedRv;

                    @Override
                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                            int viewType) {
                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
                        // Good to have colors for debugging
                        StateListDrawable stl = new StateListDrawable();
                        stl.addState(new int[]{android.R.attr.state_focused},
                                new ColorDrawable(Color.RED));
                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
                        //noinspection deprecation using this for kitkat tests
                        testViewHolder.itemView.setBackgroundDrawable(stl);
                        return testViewHolder;
                    }

                    @Override
                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
                        mAttachedRv = recyclerView;
                    }

                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        if (position < spanCount * consecutiveFocusableRowsCount) {
                            holder.itemView.setFocusable(true);
                            holder.itemView.setFocusableInTouchMode(true);
                        } else {
                            holder.itemView.setFocusable(false);
                            holder.itemView.setFocusableInTouchMode(false);
                        }
                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
                    }
                });
        waitForFirstLayout(recyclerView);

        // adapter position of the currently focused item.
        int focusIndex = 1;
        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
        View viewToFocus = toFocus.itemView;
        assertTrue(requestFocus(viewToFocus, true));
        assertSame(viewToFocus, recyclerView.getFocusedChild());

        // adapter position of the item (whether focusable or not) that just becomes fully
        // visible after focusSearch.
        int visibleIndex = focusIndex;
        // The VH of the above adapter position
        RecyclerView.ViewHolder toVisible = null;

        int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
        int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
                * spanCount + visibleIndex;

        // Navigate up through the focusable and unfocusable rows. The focusable rows should
        // become focused one by one until hitting the last focusable row, at which point,
        // unfocusable rows should become visible on the screen until the currently focused row
        // stays on the screen.
        int pos = focusIndex + spanCount;
        while (pos < itemCount) {
            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_UP, true);
            waitForIdleScroll(recyclerView);
            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);

            assertThat("Child at position " + focusIndex + " should be focused",
                    toFocus.itemView.hasFocus(), is(true));
            assertTrue("Focused child should be at least partially visible.",
                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
                    isViewFullyInBound(recyclerView, toVisible.itemView));
            pos += spanCount;
        }
    }

    @Test
    public void bottomUnfocusableViewsVisibility() throws Throwable {
        // The maximum number of rows that can be fully in-bounds of RV.
        final int visibleRowCount = 5;
        final int spanCount = 3;
        final int consecutiveFocusableRowsCount = 4;
        final int consecutiveUnFocusableRowsCount = 8;
        final int itemCount = (consecutiveFocusableRowsCount + consecutiveUnFocusableRowsCount)
                * spanCount;

        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
                        .reverseLayout(false),
                new GridTestAdapter(itemCount, 1) {
                    RecyclerView mAttachedRv;

                    @Override
                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                            int viewType) {
                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
                        // Good to have colors for debugging
                        StateListDrawable stl = new StateListDrawable();
                        stl.addState(new int[]{android.R.attr.state_focused},
                                new ColorDrawable(Color.RED));
                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
                        //noinspection deprecation using this for kitkat tests
                        testViewHolder.itemView.setBackgroundDrawable(stl);
                        return testViewHolder;
                    }

                    @Override
                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
                        mAttachedRv = recyclerView;
                    }

                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        if (position < spanCount * consecutiveFocusableRowsCount) {
                            holder.itemView.setFocusable(true);
                            holder.itemView.setFocusableInTouchMode(true);
                        } else {
                            holder.itemView.setFocusable(false);
                            holder.itemView.setFocusableInTouchMode(false);
                        }
                        holder.itemView.setMinimumHeight(mAttachedRv.getHeight() / visibleRowCount);
                    }
                });
        waitForFirstLayout(recyclerView);

        // adapter position of the currently focused item.
        int focusIndex = 1;
        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
        View viewToFocus = toFocus.itemView;
        assertTrue(requestFocus(viewToFocus, true));
        assertSame(viewToFocus, recyclerView.getFocusedChild());

        // adapter position of the item (whether focusable or not) that just becomes fully
        // visible after focusSearch.
        int visibleIndex = focusIndex;
        // The VH of the above adapter position
        RecyclerView.ViewHolder toVisible = null;

        int maxFocusIndex = (consecutiveFocusableRowsCount - 1) * spanCount + focusIndex;
        int maxVisibleIndex = (consecutiveFocusableRowsCount + visibleRowCount - 2)
                * spanCount + visibleIndex;

        // Navigate down through the focusable and unfocusable rows. The focusable rows should
        // become focused one by one until hitting the last focusable row, at which point,
        // unfocusable rows should become visible on the screen until the currently focused row
        // stays on the screen.
        int pos = focusIndex + spanCount;
        while (pos < itemCount) {
            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_DOWN, true);
            waitForIdleScroll(recyclerView);
            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);

            assertThat("Child at position " + focusIndex + " should be focused",
                    toFocus.itemView.hasFocus(), is(true));
            assertTrue("Focused child should be at least partially visible.",
                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
                    isViewFullyInBound(recyclerView, toVisible.itemView));
            pos += spanCount;
        }
    }

    @Test
    public void leftUnfocusableViewsVisibility() throws Throwable {
        // The maximum number of columns that can be fully in-bounds of RV.
        final int visibleColCount = 5;
        final int spanCount = 3;
        final int consecutiveFocusableColsCount = 4;
        final int consecutiveUnFocusableColsCount = 8;
        final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
                * spanCount;

        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
                        .orientation(HORIZONTAL).reverseLayout(true),
                new GridTestAdapter(itemCount, 1) {
                    RecyclerView mAttachedRv;

                    @Override
                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                            int viewType) {
                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
                        // Good to have colors for debugging
                        StateListDrawable stl = new StateListDrawable();
                        stl.addState(new int[]{android.R.attr.state_focused},
                                new ColorDrawable(Color.RED));
                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
                        //noinspection deprecation using this for kitkat tests
                        testViewHolder.itemView.setBackgroundDrawable(stl);
                        return testViewHolder;
                    }

                    @Override
                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
                        mAttachedRv = recyclerView;
                    }

                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        if (position < spanCount * consecutiveFocusableColsCount) {
                            holder.itemView.setFocusable(true);
                            holder.itemView.setFocusableInTouchMode(true);
                        } else {
                            holder.itemView.setFocusable(false);
                            holder.itemView.setFocusableInTouchMode(false);
                        }
                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
                    }
                });
        waitForFirstLayout(recyclerView);

        // adapter position of the currently focused item.
        int focusIndex = 1;
        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
        View viewToFocus = toFocus.itemView;
        assertTrue(requestFocus(viewToFocus, true));
        assertSame(viewToFocus, recyclerView.getFocusedChild());

        // adapter position of the item (whether focusable or not) that just becomes fully
        // visible after focusSearch.
        int visibleIndex = focusIndex;
        // The VH of the above adapter position
        RecyclerView.ViewHolder toVisible = null;

        int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
        int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
                * spanCount + visibleIndex;

        // Navigate left through the focusable and unfocusable columns. The focusable columns should
        // become focused one by one until hitting the last focusable column, at which point,
        // unfocusable columns should become visible on the screen until the currently focused
        // column stays on the screen.
        int pos = focusIndex + spanCount;
        while (pos < itemCount) {
            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_LEFT, true);
            waitForIdleScroll(recyclerView);
            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);

            assertThat("Child at position " + focusIndex + " should be focused",
                    toFocus.itemView.hasFocus(), is(true));
            assertTrue("Focused child should be at least partially visible.",
                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
                    isViewFullyInBound(recyclerView, toVisible.itemView));
            pos += spanCount;
        }
    }

    @Test
    public void rightUnfocusableViewsVisibility() throws Throwable {
        // The maximum number of columns that can be fully in-bounds of RV.
        final int visibleColCount = 5;
        final int spanCount = 3;
        final int consecutiveFocusableColsCount = 4;
        final int consecutiveUnFocusableColsCount = 8;
        final int itemCount = (consecutiveFocusableColsCount + consecutiveUnFocusableColsCount)
                * spanCount;

        final RecyclerView recyclerView = setupBasic(new Config(spanCount, itemCount)
                        .orientation(HORIZONTAL).reverseLayout(false),
                new GridTestAdapter(itemCount, 1) {
                    RecyclerView mAttachedRv;

                    @Override
                    public TestViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                            int viewType) {
                        TestViewHolder testViewHolder = super.onCreateViewHolder(parent, viewType);
                        // Good to have colors for debugging
                        StateListDrawable stl = new StateListDrawable();
                        stl.addState(new int[]{android.R.attr.state_focused},
                                new ColorDrawable(Color.RED));
                        stl.addState(StateSet.WILD_CARD, new ColorDrawable(Color.BLUE));
                        //noinspection deprecation using this for kitkat tests
                        testViewHolder.itemView.setBackgroundDrawable(stl);
                        return testViewHolder;
                    }

                    @Override
                    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
                        mAttachedRv = recyclerView;
                    }

                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        if (position < spanCount * consecutiveFocusableColsCount) {
                            holder.itemView.setFocusable(true);
                            holder.itemView.setFocusableInTouchMode(true);
                        } else {
                            holder.itemView.setFocusable(false);
                            holder.itemView.setFocusableInTouchMode(false);
                        }
                        holder.itemView.setMinimumWidth(mAttachedRv.getWidth() / visibleColCount);
                    }
                });
        waitForFirstLayout(recyclerView);

        // adapter position of the currently focused item.
        int focusIndex = 1;
        RecyclerView.ViewHolder toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
        View viewToFocus = toFocus.itemView;
        assertTrue(requestFocus(viewToFocus, true));
        assertSame(viewToFocus, recyclerView.getFocusedChild());

        // adapter position of the item (whether focusable or not) that just becomes fully
        // visible after focusSearch.
        int visibleIndex = focusIndex;
        // The VH of the above adapter position
        RecyclerView.ViewHolder toVisible = null;

        int maxFocusIndex = (consecutiveFocusableColsCount - 1) * spanCount + focusIndex;
        int maxVisibleIndex = (consecutiveFocusableColsCount + visibleColCount - 2)
                * spanCount + visibleIndex;

        // Navigate right through the focusable and unfocusable columns. The focusable columns
        // should become focused one by one until hitting the last focusable column, at which point,
        // unfocusable columns should become visible on the screen until the currently focused
        // column stays on the screen.
        int pos = focusIndex + spanCount;
        while (pos < itemCount) {
            focusSearch(recyclerView.getFocusedChild(), View.FOCUS_RIGHT, true);
            waitForIdleScroll(recyclerView);
            focusIndex = Math.min(maxFocusIndex, (focusIndex + spanCount));
            toFocus = recyclerView.findViewHolderForAdapterPosition(focusIndex);
            visibleIndex = Math.min(maxVisibleIndex, (visibleIndex + spanCount));
            toVisible = recyclerView.findViewHolderForAdapterPosition(visibleIndex);

            assertThat("Child at position " + focusIndex + " should be focused",
                    toFocus.itemView.hasFocus(), is(true));
            assertTrue("Focused child should be at least partially visible.",
                    isViewPartiallyInBound(recyclerView, toFocus.itemView));
            assertTrue("Child view at adapter pos " + visibleIndex + " should be fully visible.",
                    isViewFullyInBound(recyclerView, toVisible.itemView));
            pos += spanCount;
        }
    }

    @UiThreadTest
    @Test
    public void scrollWithoutLayout() throws Throwable {
        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
        mGlm.expectLayout(1);
        setRecyclerView(recyclerView);
        mGlm.setSpanCount(5);
        recyclerView.scrollBy(0, 10);
    }

    @Test
    public void scrollWithoutLayoutAfterInvalidate() throws Throwable {
        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
        waitForFirstLayout(recyclerView);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mGlm.setSpanCount(5);
                recyclerView.scrollBy(0, 10);
            }
        });
    }

    @Test
    public void predictiveSpanLookup1() throws Throwable {
        predictiveSpanLookupTest(0, false);
    }

    @Test
    public void predictiveSpanLookup2() throws Throwable {
        predictiveSpanLookupTest(0, true);
    }

    @Test
    public void predictiveSpanLookup3() throws Throwable {
        predictiveSpanLookupTest(1, false);
    }

    @Test
    public void predictiveSpanLookup4() throws Throwable {
        predictiveSpanLookupTest(1, true);
    }

    public void predictiveSpanLookupTest(int remaining, boolean removeFromStart) throws Throwable {
        RecyclerView recyclerView = setupBasic(new Config(3, 10));
        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                if (position < 0 || position >= mAdapter.getItemCount()) {
                    postExceptionToInstrumentation(new AssertionError("position is not within " +
                            "adapter range. pos:" + position + ", adapter size:" +
                            mAdapter.getItemCount()));
                }
                return 1;
            }

            @Override
            public int getSpanIndex(int position, int spanCount) {
                if (position < 0 || position >= mAdapter.getItemCount()) {
                    postExceptionToInstrumentation(new AssertionError("position is not within " +
                            "adapter range. pos:" + position + ", adapter size:" +
                            mAdapter.getItemCount()));
                }
                return super.getSpanIndex(position, spanCount);
            }
        });
        waitForFirstLayout(recyclerView);
        checkForMainThreadException();
        assertTrue("test sanity", mGlm.supportsPredictiveItemAnimations());
        mGlm.expectLayout(2);
        int deleteCnt = 10 - remaining;
        int deleteStart = removeFromStart ? 0 : remaining;
        mAdapter.deleteAndNotify(deleteStart, deleteCnt);
        mGlm.waitForLayout(2);
        checkForMainThreadException();
    }

    @Test
    public void movingAGroupOffScreenForAddedItems() throws Throwable {
        final RecyclerView rv = setupBasic(new Config(3, 100));
        final int[] maxId = new int[1];
        maxId[0] = -1;
        final SparseIntArray spanLookups = new SparseIntArray();
        final AtomicBoolean enableSpanLookupLogging = new AtomicBoolean(false);
        mGlm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                if (maxId[0] > 0 && mAdapter.getItemAt(position).mId > maxId[0]) {
                    return 1;
                } else if (enableSpanLookupLogging.get() && !rv.mState.isPreLayout()) {
                    spanLookups.put(position, spanLookups.get(position, 0) + 1);
                }
                return 3;
            }
        });
        ((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(true);
        waitForFirstLayout(rv);
        View lastView = rv.getChildAt(rv.getChildCount() - 1);
        final int lastPos = rv.getChildAdapterPosition(lastView);
        maxId[0] = mAdapter.getItemAt(mAdapter.getItemCount() - 1).mId;
        // now add a lot of items below this and those new views should have span size 3
        enableSpanLookupLogging.set(true);
        mGlm.expectLayout(2);
        mAdapter.addAndNotify(lastPos - 2, 30);
        mGlm.waitForLayout(2);
        checkForMainThreadException();

        assertEquals("last items span count should be queried twice", 2,
                spanLookups.get(lastPos + 30));

    }

    @Test
    public void layoutParams() throws Throwable {
        layoutParamsTest(GridLayoutManager.HORIZONTAL);
        removeRecyclerView();
        layoutParamsTest(GridLayoutManager.VERTICAL);
    }

    @Test
    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
    public void horizontalAccessibilitySpanIndices() throws Throwable {
        accessibilitySpanIndicesTest(HORIZONTAL);
    }

    @Test
    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
    public void verticalAccessibilitySpanIndices() throws Throwable {
        accessibilitySpanIndicesTest(VERTICAL);
    }

    public void accessibilitySpanIndicesTest(int orientation) throws Throwable {
        final RecyclerView recyclerView = setupBasic(new Config(3, orientation, false));
        waitForFirstLayout(recyclerView);
        final AccessibilityDelegateCompat delegateCompat = mRecyclerView
                .getCompatAccessibilityDelegate().getItemDelegate();
        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
        final View chosen = recyclerView.getChildAt(recyclerView.getChildCount() - 2);
        final int position = recyclerView.getChildLayoutPosition(chosen);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                delegateCompat.onInitializeAccessibilityNodeInfo(chosen, info);
            }
        });
        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
        AccessibilityNodeInfoCompat.CollectionItemInfoCompat itemInfo = info
                .getCollectionItemInfo();
        assertNotNull(itemInfo);
        assertEquals("result should have span group position",
                ssl.getSpanGroupIndex(position, mGlm.getSpanCount()),
                orientation == HORIZONTAL ? itemInfo.getColumnIndex() : itemInfo.getRowIndex());
        assertEquals("result should have span index",
                ssl.getSpanIndex(position, mGlm.getSpanCount()),
                orientation == HORIZONTAL ? itemInfo.getRowIndex() : itemInfo.getColumnIndex());
        assertEquals("result should have span size",
                ssl.getSpanSize(position),
                orientation == HORIZONTAL ? itemInfo.getRowSpan() : itemInfo.getColumnSpan());
    }

    public GridLayoutManager.LayoutParams ensureGridLp(View view) {
        ViewGroup.LayoutParams lp = view.getLayoutParams();
        GridLayoutManager.LayoutParams glp;
        if (lp instanceof GridLayoutManager.LayoutParams) {
            glp = (GridLayoutManager.LayoutParams) lp;
        } else if (lp == null) {
            glp = (GridLayoutManager.LayoutParams) mGlm
                    .generateDefaultLayoutParams();
            view.setLayoutParams(glp);
        } else {
            glp = (GridLayoutManager.LayoutParams) mGlm.generateLayoutParams(lp);
            view.setLayoutParams(glp);
        }
        return glp;
    }

    public void layoutParamsTest(final int orientation) throws Throwable {
        final RecyclerView rv = setupBasic(new Config(3, 100).orientation(orientation),
                new GridTestAdapter(100) {
                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
                        int val = 0;
                        switch (position % 5) {
                            case 0:
                                val = 10;
                                break;
                            case 1:
                                val = 30;
                                break;
                            case 2:
                                val = GridLayoutManager.LayoutParams.WRAP_CONTENT;
                                break;
                            case 3:
                                val = GridLayoutManager.LayoutParams.MATCH_PARENT;
                                break;
                            case 4:
                                val = 200;
                                break;
                        }
                        if (orientation == GridLayoutManager.VERTICAL) {
                            glp.height = val;
                        } else {
                            glp.width = val;
                        }
                        holder.itemView.setLayoutParams(glp);
                    }
                });
        waitForFirstLayout(rv);
        final OrientationHelper helper = mGlm.mOrientationHelper;
        final int firstRowSize = Math.max(30, getSize(mGlm.findViewByPosition(2)));
        assertEquals(firstRowSize,
                helper.getDecoratedMeasurement(mGlm.findViewByPosition(0)));
        assertEquals(firstRowSize,
                helper.getDecoratedMeasurement(mGlm.findViewByPosition(1)));
        assertEquals(firstRowSize,
                helper.getDecoratedMeasurement(mGlm.findViewByPosition(2)));
        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(0)));
        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(1)));
        assertEquals(firstRowSize, getSize(mGlm.findViewByPosition(2)));

        final int secondRowSize = Math.max(200, getSize(mGlm.findViewByPosition(3)));
        assertEquals(secondRowSize,
                helper.getDecoratedMeasurement(mGlm.findViewByPosition(3)));
        assertEquals(secondRowSize,
                helper.getDecoratedMeasurement(mGlm.findViewByPosition(4)));
        assertEquals(secondRowSize,
                helper.getDecoratedMeasurement(mGlm.findViewByPosition(5)));
        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(3)));
        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(4)));
        assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
    }

    @Test
    public void anchorUpdate() throws InterruptedException {
        GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
        final GridLayoutManager.SpanSizeLookup spanSizeLookup
                = new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                if (position > 200) {
                    return 100;
                }
                if (position > 20) {
                    return 2;
                }
                return 1;
            }
        };
        glm.setSpanSizeLookup(spanSizeLookup);
        glm.mAnchorInfo.mPosition = 11;
        RecyclerView.State state = new RecyclerView.State();
        mRecyclerView = new RecyclerView(getActivity());
        state.mItemCount = 1000;
        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
        assertEquals("gm should keep anchor in first span", 11, glm.mAnchorInfo.mPosition);

        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
        assertEquals("gm should keep anchor in last span in the row", 20,
                glm.mAnchorInfo.mPosition);

        glm.mAnchorInfo.mPosition = 5;
        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
        assertEquals("gm should keep anchor in last span in the row", 10,
                glm.mAnchorInfo.mPosition);

        glm.mAnchorInfo.mPosition = 13;
        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
        assertEquals("gm should move anchor to first span", 11, glm.mAnchorInfo.mPosition);

        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
        assertEquals("gm should keep anchor in last span in the row", 20,
                glm.mAnchorInfo.mPosition);

        glm.mAnchorInfo.mPosition = 23;
        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
        assertEquals("gm should move anchor to first span", 21, glm.mAnchorInfo.mPosition);

        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
        assertEquals("gm should keep anchor in last span in the row", 25,
                glm.mAnchorInfo.mPosition);

        glm.mAnchorInfo.mPosition = 35;
        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_TAIL);
        assertEquals("gm should move anchor to first span", 31, glm.mAnchorInfo.mPosition);
        glm.onAnchorReady(mRecyclerView.mRecycler, state, glm.mAnchorInfo,
                LinearLayoutManager.LayoutState.ITEM_DIRECTION_HEAD);
        assertEquals("gm should keep anchor in last span in the row", 35,
                glm.mAnchorInfo.mPosition);
    }

    @Test
    public void spanLookup() {
        spanLookupTest(false);
    }

    @Test
    public void spanLookupWithCache() {
        spanLookupTest(true);
    }

    @Test
    public void spanLookupCache() {
        final GridLayoutManager.SpanSizeLookup ssl
                = new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                if (position > 6) {
                    return 2;
                }
                return 1;
            }
        };
        ssl.setSpanIndexCacheEnabled(true);
        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(2));
        ssl.getCachedSpanIndex(4, 5);
        assertEquals("reference child non existent", -1, ssl.findReferenceIndexFromCache(3));
        // this should not happen and if happens, it is better to return -1
        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(5));
        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(100));
        ssl.getCachedSpanIndex(6, 5);
        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
        assertEquals("reference child before", 4, ssl.findReferenceIndexFromCache(6));
        assertEquals("reference child itself", -1, ssl.findReferenceIndexFromCache(4));
        ssl.getCachedSpanIndex(12, 5);
        assertEquals("reference child before", 12, ssl.findReferenceIndexFromCache(13));
        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(12));
        assertEquals("reference child before", 6, ssl.findReferenceIndexFromCache(7));
        for (int i = 0; i < 6; i++) {
            ssl.getCachedSpanIndex(i, 5);
        }

        for (int i = 1; i < 7; i++) {
            assertEquals("reference child right before " + i, i - 1,
                    ssl.findReferenceIndexFromCache(i));
        }
        assertEquals("reference child before 0 ", -1, ssl.findReferenceIndexFromCache(0));
    }

    public void spanLookupTest(boolean enableCache) {
        final GridLayoutManager.SpanSizeLookup ssl
                = new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                if (position > 200) {
                    return 100;
                }
                if (position > 6) {
                    return 2;
                }
                return 1;
            }
        };
        ssl.setSpanIndexCacheEnabled(enableCache);
        assertEquals(0, ssl.getCachedSpanIndex(0, 5));
        assertEquals(4, ssl.getCachedSpanIndex(4, 5));
        assertEquals(0, ssl.getCachedSpanIndex(5, 5));
        assertEquals(1, ssl.getCachedSpanIndex(6, 5));
        assertEquals(2, ssl.getCachedSpanIndex(7, 5));
        assertEquals(2, ssl.getCachedSpanIndex(9, 5));
        assertEquals(0, ssl.getCachedSpanIndex(8, 5));
    }

    @Test
    public void removeAnchorItem() throws Throwable {
        removeAnchorItemTest(
                new Config(3, 0).orientation(VERTICAL).reverseLayout(false), 100, 0);
    }

    @Test
    public void removeAnchorItemReverse() throws Throwable {
        removeAnchorItemTest(
                new Config(3, 0).orientation(VERTICAL).reverseLayout(true), 100,
                0);
    }

    @Test
    public void removeAnchorItemHorizontal() throws Throwable {
        removeAnchorItemTest(
                new Config(3, 0).orientation(HORIZONTAL).reverseLayout(
                        false), 100, 0);
    }

    @Test
    public void removeAnchorItemReverseHorizontal() throws Throwable {
        removeAnchorItemTest(
                new Config(3, 0).orientation(HORIZONTAL).reverseLayout(true),
                100, 0);
    }

    /**
     * This tests a regression where predictive animations were not working as expected when the
     * first item is removed and there aren't any more items to add from that direction.
     * First item refers to the default anchor item.
     */
    public void removeAnchorItemTest(final Config config, int adapterSize,
            final int removePos) throws Throwable {
        GridTestAdapter adapter = new GridTestAdapter(adapterSize) {
            @Override
            public void onBindViewHolder(@NonNull TestViewHolder holder,
                    int position) {
                super.onBindViewHolder(holder, position);
                ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
                if (!(lp instanceof ViewGroup.MarginLayoutParams)) {
                    lp = new ViewGroup.MarginLayoutParams(0, 0);
                    holder.itemView.setLayoutParams(lp);
                }
                ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
                final int maxSize;
                if (config.mOrientation == HORIZONTAL) {
                    maxSize = mRecyclerView.getWidth();
                    mlp.height = ViewGroup.MarginLayoutParams.MATCH_PARENT;
                } else {
                    maxSize = mRecyclerView.getHeight();
                    mlp.width = ViewGroup.MarginLayoutParams.MATCH_PARENT;
                }

                final int desiredSize;
                if (position == removePos) {
                    // make it large
                    desiredSize = maxSize / 4;
                } else {
                    // make it small
                    desiredSize = maxSize / 8;
                }
                if (config.mOrientation == HORIZONTAL) {
                    mlp.width = desiredSize;
                } else {
                    mlp.height = desiredSize;
                }
            }
        };
        RecyclerView recyclerView = setupBasic(config, adapter);
        waitForFirstLayout(recyclerView);
        final int childCount = mGlm.getChildCount();
        RecyclerView.ViewHolder toBeRemoved = null;
        List<RecyclerView.ViewHolder> toBeMoved = new ArrayList<RecyclerView.ViewHolder>();
        for (int i = 0; i < childCount; i++) {
            View child = mGlm.getChildAt(i);
            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
            if (holder.getAdapterPosition() == removePos) {
                toBeRemoved = holder;
            } else {
                toBeMoved.add(holder);
            }
        }
        assertNotNull("test sanity", toBeRemoved);
        assertEquals("test sanity", childCount - 1, toBeMoved.size());
        LoggingItemAnimator loggingItemAnimator = new LoggingItemAnimator();
        mRecyclerView.setItemAnimator(loggingItemAnimator);
        loggingItemAnimator.reset();
        loggingItemAnimator.expectRunPendingAnimationsCall(1);
        mGlm.expectLayout(2);
        adapter.deleteAndNotify(removePos, 1);
        mGlm.waitForLayout(1);
        loggingItemAnimator.waitForPendingAnimationsCall(2);
        assertTrue("removed child should receive remove animation",
                loggingItemAnimator.mRemoveVHs.contains(toBeRemoved));
        for (RecyclerView.ViewHolder vh : toBeMoved) {
            assertTrue("view holder should be in moved list",
                    loggingItemAnimator.mMoveVHs.contains(vh));
        }
        List<RecyclerView.ViewHolder> newHolders = new ArrayList<RecyclerView.ViewHolder>();
        for (int i = 0; i < mGlm.getChildCount(); i++) {
            View child = mGlm.getChildAt(i);
            RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(child);
            if (toBeRemoved != holder && !toBeMoved.contains(holder)) {
                newHolders.add(holder);
            }
        }
        assertTrue("some new children should show up for the new space", newHolders.size() > 0);
        assertEquals("no items should receive animate add since they are not new", 0,
                loggingItemAnimator.mAddVHs.size());
        for (RecyclerView.ViewHolder holder : newHolders) {
            assertTrue("new holder should receive a move animation",
                    loggingItemAnimator.mMoveVHs.contains(holder));
        }
        // for removed view, 3 for new row
        assertTrue("control against adding too many children due to bad layout state preparation."
                        + " initial:" + childCount + ", current:" + mRecyclerView.getChildCount(),
                mRecyclerView.getChildCount() <= childCount + 1 + 3);
    }

    @Test
    public void spanGroupIndex() {
        final GridLayoutManager.SpanSizeLookup ssl
                = new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                if (position > 200) {
                    return 100;
                }
                if (position > 6) {
                    return 2;
                }
                return 1;
            }
        };
        assertEquals(0, ssl.getSpanGroupIndex(0, 5));
        assertEquals(0, ssl.getSpanGroupIndex(4, 5));
        assertEquals(1, ssl.getSpanGroupIndex(5, 5));
        assertEquals(1, ssl.getSpanGroupIndex(6, 5));
        assertEquals(1, ssl.getSpanGroupIndex(7, 5));
        assertEquals(2, ssl.getSpanGroupIndex(9, 5));
        assertEquals(2, ssl.getSpanGroupIndex(8, 5));
    }

    @Test
    public void notifyDataSetChange() throws Throwable {
        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
        final GridLayoutManager.SpanSizeLookup ssl = mGlm.getSpanSizeLookup();
        ssl.setSpanIndexCacheEnabled(true);
        waitForFirstLayout(recyclerView);
        assertTrue("some positions should be cached", ssl.mSpanIndexCache.size() > 0);
        final Callback callback = new Callback() {
            @Override
            public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
                if (!state.isPreLayout()) {
                    assertEquals("cache should be empty", 0, ssl.mSpanIndexCache.size());
                }
            }

            @Override
            public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
                if (!state.isPreLayout()) {
                    assertTrue("some items should be cached", ssl.mSpanIndexCache.size() > 0);
                }
            }
        };
        mGlm.mCallbacks.add(callback);
        mGlm.expectLayout(2);
        mAdapter.deleteAndNotify(2, 3);
        mGlm.waitForLayout(2);
        checkForMainThreadException();
    }

    @Test
    public void unevenHeights() throws Throwable {
        final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
                new HashMap<Integer, RecyclerView.ViewHolder>();
        RecyclerView recyclerView = setupBasic(new Config(3, 3), new GridTestAdapter(3) {
            @Override
            public void onBindViewHolder(@NonNull TestViewHolder holder,
                    int position) {
                super.onBindViewHolder(holder, position);
                final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
                glp.height = 50 + position * 50;
                viewHolderMap.put(position, holder);
            }
        });
        waitForFirstLayout(recyclerView);
        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
            assertEquals("all items should get max height", 150,
                    vh.itemView.getHeight());
        }

        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
            assertEquals("all items should have measured the max height", 150,
                    vh.itemView.getMeasuredHeight());
        }
    }

    @Test
    public void unevenWidths() throws Throwable {
        final Map<Integer, RecyclerView.ViewHolder> viewHolderMap =
                new HashMap<Integer, RecyclerView.ViewHolder>();
        RecyclerView recyclerView = setupBasic(new Config(3, HORIZONTAL, false),
                new GridTestAdapter(3) {
                    @Override
                    public void onBindViewHolder(@NonNull TestViewHolder holder,
                            int position) {
                        super.onBindViewHolder(holder, position);
                        final GridLayoutManager.LayoutParams glp = ensureGridLp(holder.itemView);
                        glp.width = 50 + position * 50;
                        viewHolderMap.put(position, holder);
                    }
                });
        waitForFirstLayout(recyclerView);
        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
            assertEquals("all items should get max width", 150,
                    vh.itemView.getWidth());
        }

        for (RecyclerView.ViewHolder vh : viewHolderMap.values()) {
            assertEquals("all items should have measured the max width", 150,
                    vh.itemView.getMeasuredWidth());
        }
    }

    @Test
    public void spanSizeChange() throws Throwable {
        final RecyclerView rv = setupBasic(new Config(3, 100));
        waitForFirstLayout(rv);
        assertTrue(mGlm.supportsPredictiveItemAnimations());
        mGlm.expectLayout(1);
        mActivityRule.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                mGlm.setSpanCount(5);
                assertFalse(mGlm.supportsPredictiveItemAnimations());
            }
        });
        mGlm.waitForLayout(2);
        mGlm.expectLayout(2);
        mAdapter.deleteAndNotify(3, 2);
        mGlm.waitForLayout(2);
        assertTrue(mGlm.supportsPredictiveItemAnimations());
    }

    @Test
    public void cacheSpanIndices() throws Throwable {
        final RecyclerView rv = setupBasic(new Config(3, 100));
        mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
        waitForFirstLayout(rv);
        GridLayoutManager.SpanSizeLookup ssl = mGlm.mSpanSizeLookup;
        assertTrue("cache should be filled", mGlm.mSpanSizeLookup.mSpanIndexCache.size() > 0);
        assertEquals("item index 5 should be in span 2", 2,
                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
        mGlm.expectLayout(2);
        mAdapter.mFullSpanItems.add(4);
        mAdapter.changeAndNotify(4, 1);
        mGlm.waitForLayout(2);
        assertEquals("item index 5 should be in span 2", 0,
                getLp(mGlm.findViewByPosition(5)).getSpanIndex());
    }
}
