1/*
2 * Copyright (C) 2015 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 android.support.v7.widget;
18
19import org.junit.Test;
20import org.junit.runner.RunWith;
21import org.junit.runners.Parameterized;
22
23import android.graphics.Rect;
24import android.os.Parcel;
25import android.os.Parcelable;
26import android.test.suitebuilder.annotation.LargeTest;
27import android.util.Log;
28
29import java.util.ArrayList;
30import java.util.List;
31import java.util.Map;
32import java.util.UUID;
33
34import static org.junit.Assert.assertEquals;
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                boolean assignRandomSize() {
157                    return false;
158                }
159            };
160        }
161        waitFirstLayout();
162        if (mWaitForLayout) {
163            mPostLayoutOperations.run();
164        }
165        getInstrumentation().waitForIdleSync();
166        final int firstCompletelyVisiblePosition = mLayoutManager.findFirstVisibleItemPositionInt();
167        Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
168        Parcelable savedState = mRecyclerView.onSaveInstanceState();
169        // we append a suffix to the parcelable to test out of bounds
170        String parcelSuffix = UUID.randomUUID().toString();
171        Parcel parcel = Parcel.obtain();
172        savedState.writeToParcel(parcel, 0);
173        parcel.writeString(parcelSuffix);
174        removeRecyclerView();
175        // reset for reading
176        parcel.setDataPosition(0);
177        // re-create
178        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
179        removeRecyclerView();
180
181        final int itemCount = mAdapter.getItemCount();
182        List<Item> mItems = new ArrayList<>();
183        if (mLoadDataAfterRestore) {
184            mItems.addAll(mAdapter.mItems);
185            mAdapter.deleteAndNotify(0, itemCount);
186        }
187
188        RecyclerView restored = new RecyclerView(getActivity());
189        mLayoutManager = new WrappedLayoutManager(mConfig.mSpanCount, mConfig.mOrientation);
190        mLayoutManager.setGapStrategy(mConfig.mGapStrategy);
191        restored.setLayoutManager(mLayoutManager);
192        // use the same adapter for Rect matching
193        restored.setAdapter(mAdapter);
194        restored.onRestoreInstanceState(savedState);
195
196        if (mLoadDataAfterRestore) {
197            mAdapter.resetItemsTo(mItems);
198        }
199
200        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
201                parcel.readString());
202        mLayoutManager.expectLayouts(1);
203        setRecyclerView(restored);
204        mLayoutManager.waitForLayout(2);
205        assertEquals(mConfig + " on saved state, reverse layout should be preserved",
206                mConfig.mReverseLayout, mLayoutManager.getReverseLayout());
207        assertEquals(mConfig + " on saved state, orientation should be preserved",
208                mConfig.mOrientation, mLayoutManager.getOrientation());
209        assertEquals(mConfig + " on saved state, span count should be preserved",
210                mConfig.mSpanCount, mLayoutManager.getSpanCount());
211        assertEquals(mConfig + " on saved state, gap strategy should be preserved",
212                mConfig.mGapStrategy, mLayoutManager.getGapStrategy());
213        assertEquals(mConfig + " on saved state, first completely visible child position should"
214                        + " be preserved", firstCompletelyVisiblePosition,
215                mLayoutManager.findFirstVisibleItemPositionInt());
216        if (mWaitForLayout) {
217            final boolean strictItemEquality = !mLoadDataAfterRestore;
218            assertRectSetsEqual(mConfig + "\npost layout op:" + mPostLayoutOperations.describe()
219                            + ": on restore, previous view positions should be preserved",
220                    before, mLayoutManager.collectChildCoordinates(), strictItemEquality);
221        }
222        // TODO add tests for changing values after restore before layout
223    }
224
225    static abstract class PostLayoutRunnable {
226        StaggeredGridLayoutManagerSavedStateTest mTest;
227        public void setup(StaggeredGridLayoutManagerSavedStateTest test) {
228            mTest = test;
229        }
230
231        public GridTestAdapter adapter() {
232            return mTest.mAdapter;
233        }
234
235        public WrappedLayoutManager layoutManager() {
236            return mTest.mLayoutManager;
237        }
238
239        public void scrollToPositionWithOffset(int position, int offset) throws Throwable {
240            mTest.scrollToPositionWithOffset(position, offset);
241        }
242
243        public void scrollToPosition(int position) throws Throwable {
244            mTest.scrollToPosition(position);
245        }
246
247        abstract void run() throws Throwable;
248
249        abstract String describe();
250
251        @Override
252        public String toString() {
253            return describe();
254        }
255    }
256}
257