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