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 org.junit.After;
20import org.junit.Before;
21import org.junit.Test;
22import org.junit.runner.RunWith;
23
24import android.os.Looper;
25import android.support.test.runner.AndroidJUnit4;
26import android.test.ActivityInstrumentationTestCase2;
27import android.test.suitebuilder.annotation.MediumTest;
28import android.util.Log;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.TextView;
32
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.HashSet;
36import java.util.List;
37import java.util.Set;
38import java.util.concurrent.CountDownLatch;
39import java.util.concurrent.Semaphore;
40import java.util.concurrent.TimeUnit;
41import java.util.concurrent.locks.Lock;
42import java.util.concurrent.locks.ReentrantLock;
43import static org.junit.Assert.*;
44
45@MediumTest
46@RunWith(AndroidJUnit4.class)
47public class DefaultItemAnimatorTest extends BaseRecyclerViewInstrumentationTest {
48
49    private static final String TAG = "DefaultItemAnimatorTest";
50    Throwable mainThreadException;
51
52    DefaultItemAnimator mAnimator;
53    Adapter mAdapter;
54    ViewGroup mDummyParent;
55    List<RecyclerView.ViewHolder> mExpectedItems = new ArrayList<RecyclerView.ViewHolder>();
56
57    Set<RecyclerView.ViewHolder> mRemoveFinished = new HashSet<RecyclerView.ViewHolder>();
58    Set<RecyclerView.ViewHolder> mAddFinished = new HashSet<RecyclerView.ViewHolder>();
59    Set<RecyclerView.ViewHolder> mMoveFinished = new HashSet<RecyclerView.ViewHolder>();
60    Set<RecyclerView.ViewHolder> mChangeFinished = new HashSet<RecyclerView.ViewHolder>();
61
62    Semaphore mExpectedItemCount = new Semaphore(0);
63
64    @Before
65    public void setUp() throws Exception {
66        mAnimator = new DefaultItemAnimator() {
67            @Override
68            public void onRemoveFinished(RecyclerView.ViewHolder item) {
69                try {
70                    assertTrue(mRemoveFinished.add(item));
71                    onFinished(item);
72                } catch (Throwable t) {
73                    postExceptionToInstrumentation(t);
74                }
75            }
76
77            @Override
78            public void onAddFinished(RecyclerView.ViewHolder item) {
79                try {
80                    assertTrue(mAddFinished.add(item));
81                    onFinished(item);
82                } catch (Throwable t) {
83                    postExceptionToInstrumentation(t);
84                }
85            }
86
87            @Override
88            public void onMoveFinished(RecyclerView.ViewHolder item) {
89                try {
90                    assertTrue(mMoveFinished.add(item));
91                    onFinished(item);
92                } catch (Throwable t) {
93                    postExceptionToInstrumentation(t);
94                }
95            }
96
97            @Override
98            public void onChangeFinished(RecyclerView.ViewHolder item, boolean oldItem) {
99                try {
100                    assertTrue(mChangeFinished.add(item));
101                    onFinished(item);
102                } catch (Throwable t) {
103                    postExceptionToInstrumentation(t);
104                }
105            }
106
107            private void onFinished(RecyclerView.ViewHolder item) {
108                assertNotNull(mExpectedItems.remove(item));
109                mExpectedItemCount.release(1);
110            }
111        };
112        mAdapter = new Adapter(20);
113        mDummyParent = getActivity().getContainer();
114    }
115
116    void checkForMainThreadException() throws Throwable {
117        if (mainThreadException != null) {
118            throw mainThreadException;
119        }
120    }
121
122    @Test
123    public void reUseWithPayload() {
124        RecyclerView.ViewHolder vh = new ViewHolder(new TextView(getActivity()));
125        assertFalse(mAnimator.canReuseUpdatedViewHolder(vh, new ArrayList<>()));
126        assertTrue(mAnimator.canReuseUpdatedViewHolder(vh, Arrays.asList((Object) "a")));
127    }
128
129    void expectItems(RecyclerView.ViewHolder... viewHolders) {
130        mExpectedItems.addAll(Arrays.asList(viewHolders));
131    }
132
133    void runAndWait(int itemCount, int seconds) throws Throwable {
134        runAndWait(itemCount, seconds, null);
135    }
136
137    void runAndWait(int itemCount, int seconds, final ThrowingRunnable postRun) throws Throwable {
138        runTestOnUiThread(new Runnable() {
139            @Override
140            public void run() {
141                mAnimator.runPendingAnimations();
142                if (postRun != null) {
143                    try {
144                        postRun.run();
145                    } catch (Throwable e) {
146                        throw new RuntimeException(e);
147                    }
148                }
149            }
150        });
151        waitForItems(itemCount, seconds);
152        checkForMainThreadException();
153    }
154
155    void waitForItems(int itemCount, int seconds) throws InterruptedException {
156        assertTrue("all vh animations should end",
157                mExpectedItemCount.tryAcquire(itemCount, seconds, TimeUnit.SECONDS));
158        assertEquals("all expected finish events should happen", 0, mExpectedItems.size());
159        // wait one more second for unwanted
160        assertFalse("should not receive any more permits",
161                mExpectedItemCount.tryAcquire(1, 2, TimeUnit.SECONDS));
162    }
163
164    @Test
165    public void animateAdd() throws Throwable {
166        ViewHolder vh = createViewHolder(1);
167        expectItems(vh);
168        assertTrue(animateAdd(vh));
169        assertTrue(mAnimator.isRunning());
170        runAndWait(1, 1);
171    }
172
173    @Test
174    public void animateRemove() throws Throwable {
175        ViewHolder vh = createViewHolder(1);
176        expectItems(vh);
177        assertTrue(animateRemove(vh));
178        assertTrue(mAnimator.isRunning());
179        runAndWait(1, 1);
180    }
181
182    @Test
183    public void animateMove() throws Throwable {
184        ViewHolder vh = createViewHolder(1);
185        expectItems(vh);
186        assertTrue(animateMove(vh, 0, 0, 100, 100));
187        assertTrue(mAnimator.isRunning());
188        runAndWait(1, 1);
189    }
190
191    @Test
192    public void animateChange() throws Throwable {
193        ViewHolder vh = createViewHolder(1);
194        ViewHolder vh2 = createViewHolder(2);
195        expectItems(vh, vh2);
196        assertTrue(animateChange(vh, vh2, 0, 0, 100, 100));
197        assertTrue(mAnimator.isRunning());
198        runAndWait(2, 1);
199    }
200
201    public void cancelBefore(int count, final RecyclerView.ViewHolder... toCancel)
202            throws Throwable {
203        cancelTest(true, count, toCancel);
204    }
205
206    public void cancelAfter(int count, final RecyclerView.ViewHolder... toCancel)
207            throws Throwable {
208        cancelTest(false, count, toCancel);
209    }
210
211    public void cancelTest(boolean before, int count, final RecyclerView.ViewHolder... toCancel) throws Throwable {
212        if (before) {
213            endAnimations(toCancel);
214            runAndWait(count, 1);
215        } else {
216            runAndWait(count, 1, new ThrowingRunnable() {
217                @Override
218                public void run() throws Throwable {
219                    endAnimations(toCancel);
220                }
221            });
222        }
223    }
224
225    @Test
226    public void cancelAddBefore() throws Throwable {
227        final ViewHolder vh = createViewHolder(1);
228        expectItems(vh);
229        assertTrue(animateAdd(vh));
230        cancelBefore(1, vh);
231    }
232
233    @Test
234    public void cancelAddAfter() throws Throwable {
235        final ViewHolder vh = createViewHolder(1);
236        expectItems(vh);
237        assertTrue(animateAdd(vh));
238        cancelAfter(1, vh);
239    }
240
241    @Test
242    public void cancelMoveBefore() throws Throwable {
243        ViewHolder vh = createViewHolder(1);
244        expectItems(vh);
245        assertTrue(animateMove(vh, 10, 10, 100, 100));
246        cancelBefore(1, vh);
247    }
248
249    @Test
250    public void cancelMoveAfter() throws Throwable {
251        ViewHolder vh = createViewHolder(1);
252        expectItems(vh);
253        assertTrue(animateMove(vh, 10, 10, 100, 100));
254        cancelAfter(1, vh);
255    }
256
257    @Test
258    public void cancelRemove() throws Throwable {
259        ViewHolder vh = createViewHolder(1);
260        expectItems(vh);
261        assertTrue(animateRemove(vh));
262        endAnimations(vh);
263        runAndWait(1, 1);
264    }
265
266    @Test
267    public void cancelChangeOldBefore() throws Throwable {
268        cancelChangeOldTest(true);
269    }
270    @Test
271    public void cancelChangeOldAfter() throws Throwable {
272        cancelChangeOldTest(false);
273    }
274
275    public void cancelChangeOldTest(boolean before) throws Throwable {
276        ViewHolder vh = createViewHolder(1);
277        ViewHolder vh2 = createViewHolder(1);
278        expectItems(vh, vh2);
279        assertTrue(animateChange(vh, vh2, 20, 20, 100, 100));
280        cancelTest(before, 2, vh);
281    }
282
283    @Test
284    public void cancelChangeNewBefore() throws Throwable {
285        cancelChangeNewTest(true);
286    }
287
288    @Test
289    public void cancelChangeNewAfter() throws Throwable {
290        cancelChangeNewTest(false);
291    }
292
293    public void cancelChangeNewTest(boolean before) throws Throwable {
294        ViewHolder vh = createViewHolder(1);
295        ViewHolder vh2 = createViewHolder(1);
296        expectItems(vh, vh2);
297        assertTrue(animateChange(vh, vh2, 20, 20, 100, 100));
298        cancelTest(before, 2, vh2);
299    }
300
301    @Test
302    public void cancelChangeBothBefore() throws Throwable {
303        cancelChangeBothTest(true);
304    }
305
306    @Test
307    public void cancelChangeBothAfter() throws Throwable {
308        cancelChangeBothTest(false);
309    }
310
311    public void cancelChangeBothTest(boolean before) throws Throwable {
312        ViewHolder vh = createViewHolder(1);
313        ViewHolder vh2 = createViewHolder(1);
314        expectItems(vh, vh2);
315        assertTrue(animateChange(vh, vh2, 20, 20, 100, 100));
316        cancelTest(before, 2, vh, vh2);
317    }
318
319    void endAnimations(final RecyclerView.ViewHolder... vhs) throws Throwable {
320        runTestOnUiThread(new Runnable() {
321            @Override
322            public void run() {
323                for (RecyclerView.ViewHolder vh : vhs) {
324                    mAnimator.endAnimation(vh);
325                }
326            }
327        });
328    }
329
330    boolean animateAdd(final RecyclerView.ViewHolder vh) throws Throwable {
331        final boolean[] result = new boolean[1];
332        runTestOnUiThread(new Runnable() {
333            @Override
334            public void run() {
335                result[0] = mAnimator.animateAdd(vh);
336            }
337        });
338        return result[0];
339    }
340
341    boolean animateRemove(final RecyclerView.ViewHolder vh) throws Throwable {
342        final boolean[] result = new boolean[1];
343        runTestOnUiThread(new Runnable() {
344            @Override
345            public void run() {
346                result[0] = mAnimator.animateRemove(vh);
347            }
348        });
349        return result[0];
350    }
351
352    boolean animateMove(final RecyclerView.ViewHolder vh, final int fromX, final int fromY,
353            final int toX, final int toY) throws Throwable {
354        final boolean[] result = new boolean[1];
355        runTestOnUiThread(new Runnable() {
356            @Override
357            public void run() {
358                result[0] = mAnimator.animateMove(vh, fromX, fromY, toX, toY);
359            }
360        });
361        return result[0];
362    }
363
364    boolean animateChange(final RecyclerView.ViewHolder oldHolder,
365            final RecyclerView.ViewHolder newHolder,
366            final int fromX, final int fromY, final int toX, final int toY) throws Throwable {
367        final boolean[] result = new boolean[1];
368        runTestOnUiThread(new Runnable() {
369            @Override
370            public void run() {
371                result[0] = mAnimator.animateChange(oldHolder, newHolder, fromX, fromY, toX, toY);
372            }
373        });
374        return result[0];
375    }
376
377    private ViewHolder createViewHolder(final int pos) throws Throwable {
378        final ViewHolder vh = mAdapter.createViewHolder(mDummyParent, 1);
379        runTestOnUiThread(new Runnable() {
380            @Override
381            public void run() {
382                mAdapter.bindViewHolder(vh, pos);
383                mDummyParent.addView(vh.itemView);
384            }
385        });
386
387        return vh;
388    }
389
390    void postExceptionToInstrumentation(Throwable t) {
391        if (mainThreadException == null) {
392            mainThreadException = t;
393        } else {
394            Log.e(TAG, "skipping secondary main thread exception", t);
395        }
396    }
397
398
399    private class Adapter extends RecyclerView.Adapter<ViewHolder> {
400
401        List<String> mItems;
402
403        private Adapter(int count) {
404            mItems = new ArrayList<>();
405            for (int i = 0; i < count; i++) {
406                mItems.add("item-" + i);
407            }
408        }
409
410        @Override
411        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
412            return new ViewHolder(new TextView(parent.getContext()));
413        }
414
415        @Override
416        public void onBindViewHolder(ViewHolder holder, int position) {
417            holder.bind(mItems.get(position));
418        }
419
420        @Override
421        public int getItemCount() {
422            return mItems.size();
423        }
424    }
425
426    private class ViewHolder extends RecyclerView.ViewHolder {
427
428        String mBindedText;
429
430        public ViewHolder(View itemView) {
431            super(itemView);
432        }
433
434        public void bind(String text) {
435            mBindedText = text;
436            ((TextView) itemView).setText(text);
437        }
438    }
439
440    private interface ThrowingRunnable {
441        void run() throws Throwable;
442    }
443
444    @Override
445    public void runTestOnUiThread(Runnable r) throws Throwable {
446        if (Looper.myLooper() == Looper.getMainLooper()) {
447            r.run();
448        } else {
449            super.runTestOnUiThread(r);
450        }
451    }
452}
453