1/*
2 * Copyright (C) 2014 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 android.os.Parcel;
20import android.os.Parcelable;
21import android.test.AndroidTestCase;
22import android.util.Log;
23import android.view.View;
24import android.view.ViewGroup;
25import android.widget.TextView;
26
27import java.util.UUID;
28
29public class RecyclerViewBasicTest extends AndroidTestCase {
30
31    RecyclerView mRecyclerView;
32
33    @Override
34    protected void setUp() throws Exception {
35        super.setUp();
36        mRecyclerView = new RecyclerView(mContext);
37    }
38
39    public void testMeasureWithoutLayoutManager() {
40        measure();
41    }
42
43    private void measure() {
44        mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240);
45    }
46
47    private void layout() {
48        mRecyclerView.layout(0, 0, 320, 320);
49    }
50
51    private void focusSearch() {
52        mRecyclerView.focusSearch(1);
53    }
54
55    public void testLayoutWithoutAdapter() throws InterruptedException {
56        MockLayoutManager layoutManager = new MockLayoutManager();
57        mRecyclerView.setLayoutManager(layoutManager);
58        layout();
59        assertEquals("layout manager should not be called if there is no adapter attached",
60                0, layoutManager.mLayoutCount);
61    }
62
63    public void testLayoutWithoutLayoutManager() throws InterruptedException {
64        mRecyclerView.setAdapter(new MockAdapter(20));
65        measure();
66        layout();
67    }
68
69    public void testFocusWithoutLayoutManager() throws InterruptedException {
70        mRecyclerView.setAdapter(new MockAdapter(20));
71        measure();
72        layout();
73        focusSearch();
74    }
75
76    public void testScrollWithoutLayoutManager() throws InterruptedException {
77        mRecyclerView.setAdapter(new MockAdapter(20));
78        measure();
79        layout();
80        mRecyclerView.scrollBy(10, 10);
81    }
82
83    public void testSmoothScrollWithoutLayoutManager() throws InterruptedException {
84        mRecyclerView.setAdapter(new MockAdapter(20));
85        measure();
86        layout();
87        mRecyclerView.smoothScrollBy(10, 10);
88    }
89
90    public void testScrollToPositionWithoutLayoutManager() throws InterruptedException {
91        mRecyclerView.setAdapter(new MockAdapter(20));
92        measure();
93        layout();
94        mRecyclerView.scrollToPosition(5);
95    }
96
97    public void testSmoothScrollToPositionWithoutLayoutManager() throws InterruptedException {
98        mRecyclerView.setAdapter(new MockAdapter(20));
99        measure();
100        layout();
101        mRecyclerView.smoothScrollToPosition(5);
102    }
103
104    public void testLayout() throws InterruptedException {
105        MockLayoutManager layoutManager = new MockLayoutManager();
106        mRecyclerView.setLayoutManager(layoutManager);
107        mRecyclerView.setAdapter(new MockAdapter(3));
108        layout();
109        assertEquals("when both layout manager and activity is set, recycler view should call"
110                + " layout manager's layout method", 1, layoutManager.mLayoutCount);
111    }
112
113    public void testObservingAdapters() {
114        MockAdapter adapterOld = new MockAdapter(1);
115        mRecyclerView.setAdapter(adapterOld);
116        assertTrue("attached adapter should have observables", adapterOld.hasObservers());
117
118        MockAdapter adapterNew = new MockAdapter(2);
119        mRecyclerView.setAdapter(adapterNew);
120        assertFalse("detached adapter should lose observable", adapterOld.hasObservers());
121        assertTrue("new adapter should have observers", adapterNew.hasObservers());
122
123        mRecyclerView.setAdapter(null);
124        assertNull("adapter should be removed successfully", mRecyclerView.getAdapter());
125        assertFalse("when adapter is removed, observables should be removed too",
126                adapterNew.hasObservers());
127    }
128
129    public void testAdapterChangeCallbacks() {
130        MockLayoutManager layoutManager = new MockLayoutManager();
131        mRecyclerView.setLayoutManager(layoutManager);
132        MockAdapter adapterOld = new MockAdapter(1);
133        mRecyclerView.setAdapter(adapterOld);
134        layoutManager.assertPrevNextAdapters(null, adapterOld);
135
136        MockAdapter adapterNew = new MockAdapter(2);
137        mRecyclerView.setAdapter(adapterNew);
138        layoutManager.assertPrevNextAdapters("switching adapters should trigger correct callbacks"
139                , adapterOld, adapterNew);
140
141        mRecyclerView.setAdapter(null);
142        layoutManager.assertPrevNextAdapters(
143                "Setting adapter null should trigger correct callbacks",
144                adapterNew, null);
145    }
146
147    public void testSavedStateWithStatelessLayoutManager() throws InterruptedException {
148        mRecyclerView.setLayoutManager(new MockLayoutManager() {
149            @Override
150            public Parcelable onSaveInstanceState() {
151                return null;
152            }
153        });
154        mRecyclerView.setAdapter(new MockAdapter(3));
155        Parcel parcel = Parcel.obtain();
156        String parcelSuffix = UUID.randomUUID().toString();
157        Parcelable savedState = mRecyclerView.onSaveInstanceState();
158        savedState.writeToParcel(parcel, 0);
159        parcel.writeString(parcelSuffix);
160
161        // reset position for reading
162        parcel.setDataPosition(0);
163        RecyclerView restored = new RecyclerView(mContext);
164        restored.setLayoutManager(new MockLayoutManager());
165        mRecyclerView.setAdapter(new MockAdapter(3));
166        // restore
167        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
168        restored.onRestoreInstanceState(savedState);
169
170        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
171                parcel.readString());
172        assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail());
173
174    }
175
176    public void testSavedState() throws InterruptedException {
177        MockLayoutManager mlm = new MockLayoutManager();
178        mRecyclerView.setLayoutManager(mlm);
179        mRecyclerView.setAdapter(new MockAdapter(3));
180        layout();
181        Parcelable savedState = mRecyclerView.onSaveInstanceState();
182        // we append a suffix to the parcelable to test out of bounds
183        String parcelSuffix = UUID.randomUUID().toString();
184        Parcel parcel = Parcel.obtain();
185        savedState.writeToParcel(parcel, 0);
186        parcel.writeString(parcelSuffix);
187
188        // reset for reading
189        parcel.setDataPosition(0);
190        // re-create
191        savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
192
193        RecyclerView restored = new RecyclerView(mContext);
194        MockLayoutManager mlmRestored = new MockLayoutManager();
195        restored.setLayoutManager(mlmRestored);
196        restored.setAdapter(new MockAdapter(3));
197        restored.onRestoreInstanceState(savedState);
198
199        assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
200                parcel.readString());
201        assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail());
202        assertEquals("uuid in layout manager should be preserved properly", mlm.mUuid,
203                mlmRestored.mUuid);
204        assertNotSame("stateless parameter should not be preserved", mlm.mLayoutCount,
205                mlmRestored.mLayoutCount);
206        layout();
207    }
208
209    static class MockLayoutManager extends RecyclerView.LayoutManager {
210
211        int mLayoutCount = 0;
212
213        int mAdapterChangedCount = 0;
214
215        RecyclerView.Adapter mPrevAdapter;
216
217        RecyclerView.Adapter mNextAdapter;
218
219        String mUuid = UUID.randomUUID().toString();
220
221        @Override
222        public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
223                RecyclerView.Adapter newAdapter) {
224            super.onAdapterChanged(oldAdapter, newAdapter);
225            mPrevAdapter = oldAdapter;
226            mNextAdapter = newAdapter;
227            mAdapterChangedCount++;
228        }
229
230        @Override
231        public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
232            mLayoutCount += 1;
233        }
234
235        @Override
236        public Parcelable onSaveInstanceState() {
237            LayoutManagerSavedState lss = new LayoutManagerSavedState();
238            lss.mUuid = mUuid;
239            return lss;
240        }
241
242        @Override
243        public void onRestoreInstanceState(Parcelable state) {
244            super.onRestoreInstanceState(state);
245            if (state instanceof LayoutManagerSavedState) {
246                mUuid = ((LayoutManagerSavedState) state).mUuid;
247            }
248        }
249
250        @Override
251        public RecyclerView.LayoutParams generateDefaultLayoutParams() {
252            return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
253                    ViewGroup.LayoutParams.WRAP_CONTENT);
254        }
255
256        public void assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter,
257                RecyclerView.Adapter nextAdapter) {
258            assertSame(message, prevAdapter, mPrevAdapter);
259            assertSame(message, nextAdapter, mNextAdapter);
260        }
261
262        public void assertPrevNextAdapters(RecyclerView.Adapter prevAdapter,
263                RecyclerView.Adapter nextAdapter) {
264            assertPrevNextAdapters("Adapters from onAdapterChanged callback should match",
265                    prevAdapter, nextAdapter);
266        }
267
268        @Override
269        public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
270                RecyclerView.State state) {
271            return dx;
272        }
273
274        @Override
275        public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
276                RecyclerView.State state) {
277            return dy;
278        }
279
280        @Override
281        public boolean canScrollHorizontally() {
282            return true;
283        }
284
285        @Override
286        public boolean canScrollVertically() {
287            return true;
288        }
289    }
290
291    static class LayoutManagerSavedState implements Parcelable {
292
293        String mUuid;
294
295        public LayoutManagerSavedState(Parcel in) {
296            mUuid = in.readString();
297        }
298
299        public LayoutManagerSavedState() {
300
301        }
302
303        @Override
304        public int describeContents() {
305            return 0;
306        }
307
308        @Override
309        public void writeToParcel(Parcel dest, int flags) {
310            dest.writeString(mUuid);
311        }
312
313        public static final Parcelable.Creator<LayoutManagerSavedState> CREATOR
314                = new Parcelable.Creator<LayoutManagerSavedState>() {
315            @Override
316            public LayoutManagerSavedState createFromParcel(Parcel in) {
317                return new LayoutManagerSavedState(in);
318            }
319
320            @Override
321            public LayoutManagerSavedState[] newArray(int size) {
322                return new LayoutManagerSavedState[size];
323            }
324        };
325    }
326
327    static class MockAdapter extends RecyclerView.Adapter {
328
329        private int mCount = 0;
330
331
332        MockAdapter(int count) {
333            this.mCount = count;
334        }
335
336        @Override
337        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
338            return new MockViewHolder(new TextView(parent.getContext()));
339        }
340
341        @Override
342        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
343
344        }
345
346        @Override
347        public int getItemCount() {
348            return mCount;
349        }
350
351        void removeItems(int start, int count) {
352            mCount -= count;
353            notifyItemRangeRemoved(start, count);
354        }
355
356        void addItems(int start, int count) {
357            mCount += count;
358            notifyItemRangeInserted(start, count);
359        }
360    }
361
362    static class MockViewHolder extends RecyclerView.ViewHolder {
363        public Object mItem;
364        public MockViewHolder(View itemView) {
365            super(itemView);
366        }
367    }
368}