1/*
2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.recyclerview.widget;
18
19import static org.junit.Assert.assertEquals;
20
21import android.graphics.Rect;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.support.test.filters.LargeTest;
25import android.util.Log;
26
27import org.junit.Test;
28import org.junit.runner.RunWith;
29import org.junit.runners.Parameterized;
30
31import java.util.ArrayList;
32import java.util.List;
33import java.util.Map;
34import java.util.UUID;
35
36@RunWith(Parameterized.class)
37@LargeTest
38public class StaggeredGridLayoutManagerSavedStateTest extends BaseStaggeredGridLayoutManagerTest {
39    private final Config mConfig;
40    private final boolean mWaitForLayout;
41    private final boolean mLoadDataAfterRestore;
42    private final PostLayoutRunnable mPostLayoutOperations;
43
44    public StaggeredGridLayoutManagerSavedStateTest(
45            Config config, boolean waitForLayout, boolean loadDataAfterRestore,
46            PostLayoutRunnable postLayoutOperations) throws CloneNotSupportedException {
47        this.mConfig = (Config) config.clone();
48        this.mWaitForLayout = waitForLayout;
49        this.mLoadDataAfterRestore = loadDataAfterRestore;
50        this.mPostLayoutOperations = postLayoutOperations;
51        if (postLayoutOperations != null) {
52            postLayoutOperations.mTest = this;
53        }
54    }
55
56    @Parameterized.Parameters(name = "config={0},waitForLayout={1},loadDataAfterRestore={2}"
57            + ",postLayoutRunnable={3}")
58    public static List<Object[]> getParams() throws CloneNotSupportedException {
59        List<Config> variations = createBaseVariations();
60
61        PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
62                new PostLayoutRunnable() {
63                    @Override
64                    public void run() throws Throwable {
65                        // do nothing
66                    }
67
68                    @Override
69                    public String describe() {
70                        return "doing_nothing";
71                    }
72                },
73                new PostLayoutRunnable() {
74                    @Override
75                    public void run() throws Throwable {
76                        layoutManager().expectLayouts(1);
77                        scrollToPosition(adapter().getItemCount() * 3 / 4);
78                        layoutManager().waitForLayout(2);
79                    }
80
81                    @Override
82                    public String describe() {
83                        return "scroll_to_position_item_count*3/4";
84                    }
85                },
86                new PostLayoutRunnable() {
87                    @Override
88                    public void run() throws Throwable {
89                        layoutManager().expectLayouts(1);
90                        scrollToPositionWithOffset(adapter().getItemCount() / 3,
91                                50);
92                        layoutManager().waitForLayout(2);
93                    }
94
95                    @Override
96                    public String describe() {
97                        return "scroll_to_position_item_count/3_with_positive_offset";
98                    }
99                },
100                new PostLayoutRunnable() {
101                    @Override
102                    public void run() throws Throwable {
103                        layoutManager().expectLayouts(1);
104                        scrollToPositionWithOffset(adapter().getItemCount() * 2 / 3,
105                                -50);
106                        layoutManager().waitForLayout(2);
107                    }
108
109                    @Override
110                    public String describe() {
111                        return "scroll_to_position_with_negative_offset";
112                    }
113                }
114        };
115        boolean[] waitForLayoutOptions = new boolean[]{false, true};
116        boolean[] loadDataAfterRestoreOptions = new boolean[]{false, true};
117        List<Config> testVariations = new ArrayList<Config>();
118        testVariations.addAll(variations);
119        for (Config config : variations) {
120            if (config.mSpanCount < 2) {
121                continue;
122            }
123            final Config clone = (Config) config.clone();
124            clone.mItemCount = clone.mSpanCount - 1;
125            testVariations.add(clone);
126        }
127
128        List<Object[]> params = new ArrayList<>();
129        for (Config config : testVariations) {
130            for (PostLayoutRunnable runnable : postLayoutOptions) {
131                for (boolean waitForLayout : waitForLayoutOptions) {
132                    for (boolean loadDataAfterRestore : loadDataAfterRestoreOptions) {
133                        params.add(new Object[]{config, waitForLayout, loadDataAfterRestore,
134                                runnable});
135                    }
136                }
137            }
138        }
139        return params;
140    }
141
142    @Test
143    public void savedState() throws Throwable {
144        if (DEBUG) {
145            Log.d(TAG, "testing saved state with wait for layout = " + mWaitForLayout + " config "
146                    + mConfig + " post layout action " + mPostLayoutOperations.describe());
147        }
148        setupByConfig(mConfig);
149        if (mLoadDataAfterRestore) {
150            // We are going to re-create items, force non-random item size.
151            mAdapter.mOnBindCallback = new OnBindCallback() {
152                @Override
153                void onBoundItem(TestViewHolder vh, int position) {
154                }
155
156                @Override
157                boolean assignRandomSize() {
158                    return false;
159                }
160            };
161        }
162        waitFirstLayout();
163        if (mWaitForLayout) {
164            mPostLayoutOperations.run();
165        }
166        getInstrumentation().waitForIdleSync();
167        final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt();
168        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
169        Parcelable savedState = mRecyclerView.onSaveInstanceState();
170        // we append a suffix to the parcelable to test out of bounds
171        String parcelSuffix = UUID.randomUUID().toString();
172        Parcel parcel = Parcel.obtain();
173        savedState.writeToParcel(parcel, 0);
174        parcel.writeString(parcelSuffix);
175        removeRecyclerView();
176        // reset for reading
177        parcel.setDataPosition(0);
178        // re-create
179        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
180        removeRecyclerView();
181
182        final int itemCount = mAdapter.getItemCount();
183        List<Item> mItems = new ArrayList<>();
184        if (mLoadDataAfterRestore) {
185            mItems.addAll(mAdapter.mItems);
186            mAdapter.deleteAndNotify(0, itemCount);
187        }
188
189        RecyclerView restored = new RecyclerView(getActivity());
190        mLayoutManager = new WrappedLayoutManager(mConfig.mSpanCount, mConfig.mOrientation);
191        mLayoutManager.setGapStrategy(mConfig.mGapStrategy);
192        restored.setLayoutManager(mLayoutManager);
193        // use the same adapter for Rect matching
194        restored.setAdapter(mAdapter);
195        restored.onRestoreInstanceState(savedState);
196
197        if (mLoadDataAfterRestore) {
198            mAdapter.resetItemsTo(mItems);
199        }
200
201        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
202                parcel.readString());
203        mLayoutManager.expectLayouts(1);
204        setRecyclerView(restored);
205        mLayoutManager.waitForLayout(2);
206        assertEquals(mConfig + " on saved state, reverse layout should be preserved",
207                mConfig.mReverseLayout, mLayoutManager.getReverseLayout());
208        assertEquals(mConfig + " on saved state, orientation should be preserved",
209                mConfig.mOrientation, mLayoutManager.getOrientation());
210        assertEquals(mConfig + " on saved state, span count should be preserved",
211                mConfig.mSpanCount, mLayoutManager.getSpanCount());
212        assertEquals(mConfig + " on saved state, gap strategy should be preserved",
213                mConfig.mGapStrategy, mLayoutManager.getGapStrategy());
214        assertEquals(mConfig + " on saved state, first completely visible child position should"
215                        + " be preserved", firstCompletelyVisiblePosition,
216                mLayoutManager.findFirstVisibleItemPositionInt());
217        if (mWaitForLayout) {
218            final boolean strictItemEquality = !mLoadDataAfterRestore;
219            assertRectSetsEqual(mConfig + "\npost layout op:" + mPostLayoutOperations.describe()
220                            + ": on restore, previous view positions should be preserved",
221                    before, mLayoutManager.collectChildCoordinates(), strictItemEquality);
222        }
223        // TODO add tests for changing values after restore before layout
224    }
225
226    static abstract class PostLayoutRunnable {
227        StaggeredGridLayoutManagerSavedStateTest mTest;
228        public void setup(StaggeredGridLayoutManagerSavedStateTest test) {
229            mTest = test;
230        }
231
232        public GridTestAdapter adapter() {
233            return mTest.mAdapter;
234        }
235
236        public WrappedLayoutManager layoutManager() {
237            return mTest.mLayoutManager;
238        }
239
240        public void scrollToPositionWithOffset(int position, int offset) throws Throwable {
241            mTest.scrollToPositionWithOffset(position, offset);
242        }
243
244        public void scrollToPosition(int position) throws Throwable {
245            mTest.scrollToPosition(position);
246        }
247
248        abstract void run() throws Throwable;
249
250        abstract String describe();
251
252        @Override
253        public String toString() {
254            return describe();
255        }
256    }
257}
258