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 androidx.recyclerview.widget.ItemTouchHelper.END;
20import static androidx.recyclerview.widget.ItemTouchHelper.LEFT;
21import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
22import static androidx.recyclerview.widget.ItemTouchHelper.START;
23import static androidx.recyclerview.widget.ItemTouchHelper.SimpleCallback;
24
25import static org.junit.Assert.assertEquals;
26import static org.junit.Assert.assertNotNull;
27import static org.junit.Assert.assertTrue;
28
29import android.os.Build;
30import android.support.test.filters.LargeTest;
31import android.support.test.filters.SdkSuppress;
32import android.support.test.filters.Suppress;
33import android.support.test.runner.AndroidJUnit4;
34import android.view.Gravity;
35import android.view.View;
36
37import androidx.annotation.NonNull;
38import androidx.core.util.Pair;
39import androidx.testutils.PollingCheck;
40
41import org.junit.Test;
42import org.junit.runner.RunWith;
43
44import java.util.ArrayList;
45import java.util.List;
46
47@LargeTest
48@RunWith(AndroidJUnit4.class)
49public class ItemTouchHelperTest extends BaseRecyclerViewInstrumentationTest {
50
51    private static class RecyclerViewState {
52        public TestAdapter mAdapter;
53        public TestLayoutManager mLayoutManager;
54        public WrappedRecyclerView mWrappedRecyclerView;
55    }
56
57    private LoggingCalback mCalback;
58
59    private LoggingItemTouchHelper mItemTouchHelper;
60
61    private Boolean mSetupRTL;
62
63    public ItemTouchHelperTest() {
64        super(false);
65    }
66
67    private RecyclerViewState setupRecyclerView() throws Throwable {
68        RecyclerViewState rvs = new RecyclerViewState();
69        rvs.mWrappedRecyclerView = inflateWrappedRV();
70        rvs.mAdapter = new TestAdapter(10);
71        rvs.mLayoutManager = new TestLayoutManager() {
72            @Override
73            public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
74                detachAndScrapAttachedViews(recycler);
75                layoutRange(recycler, 0, Math.min(5, state.getItemCount()));
76                layoutLatch.countDown();
77            }
78
79            @Override
80            public boolean canScrollHorizontally() {
81                return false;
82            }
83
84            @Override
85            public boolean supportsPredictiveItemAnimations() {
86                return false;
87            }
88        };
89        rvs.mWrappedRecyclerView.setFakeRTL(mSetupRTL);
90        rvs.mWrappedRecyclerView.setAdapter(rvs.mAdapter);
91        rvs.mWrappedRecyclerView.setLayoutManager(rvs.mLayoutManager);
92        return rvs;
93    }
94
95    private RecyclerViewState setupItemTouchHelper(final RecyclerViewState rvs, int dragDirs,
96            int swipeDirs) throws Throwable {
97        mCalback = new LoggingCalback(dragDirs, swipeDirs);
98        mItemTouchHelper = new LoggingItemTouchHelper(mCalback);
99        mActivityRule.runOnUiThread(new Runnable() {
100            @Override
101            public void run() {
102                mItemTouchHelper.attachToRecyclerView(rvs.mWrappedRecyclerView);
103            }
104        });
105
106        return rvs;
107    }
108
109    @Test
110    public void swipeLeft() throws Throwable {
111        basicSwipeTest(LEFT, LEFT | RIGHT, -getActivity().getWindow().getDecorView().getWidth());
112    }
113
114    @Test
115    public void swipeRight() throws Throwable {
116        basicSwipeTest(RIGHT, LEFT | RIGHT, getActivity().getWindow().getDecorView().getWidth());
117    }
118
119    @Test
120    public void swipeStart() throws Throwable {
121        basicSwipeTest(START, START | END, -getActivity().getWindow().getDecorView().getWidth());
122    }
123
124    @Test
125    public void swipeEnd() throws Throwable {
126        basicSwipeTest(END, START | END, getActivity().getWindow().getDecorView().getWidth());
127    }
128
129    // Test is disabled as it is flaky.
130    @Suppress
131    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
132    @Test
133    public void swipeStartInRTL() throws Throwable {
134        mSetupRTL = true;
135        basicSwipeTest(START, START | END, getActivity().getWindow().getDecorView().getWidth());
136    }
137
138    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN_MR1)
139    @Test
140    public void swipeEndInRTL() throws Throwable {
141        mSetupRTL = true;
142        basicSwipeTest(END, START | END, -getActivity().getWindow().getDecorView().getWidth());
143    }
144
145    @Test
146    public void attachToNullRecycleViewDuringLongPress() throws Throwable {
147        final RecyclerViewState rvs = setupItemTouchHelper(setupRecyclerView(), END, 0);
148        rvs.mLayoutManager.expectLayouts(1);
149        setRecyclerView(rvs.mWrappedRecyclerView);
150        rvs.mLayoutManager.waitForLayout(1);
151
152        final RecyclerView.ViewHolder target = mRecyclerView
153                .findViewHolderForAdapterPosition(1);
154        target.itemView.setOnLongClickListener(new View.OnLongClickListener() {
155            @Override
156            public boolean onLongClick(View v) {
157                mItemTouchHelper.attachToRecyclerView(null);
158                return false;
159            }
160        });
161        TouchUtils.longClickView(getInstrumentation(), target.itemView);
162    }
163
164    @Test
165    public void attachToAnotherRecycleViewDuringLongPress() throws Throwable {
166        final RecyclerViewState rvs2 = setupRecyclerView();
167        rvs2.mLayoutManager.expectLayouts(1);
168        mActivityRule.runOnUiThread(new Runnable() {
169            @Override
170            public void run() {
171                getActivity().getContainer().addView(rvs2.mWrappedRecyclerView);
172            }
173        });
174        rvs2.mLayoutManager.waitForLayout(1);
175
176        final RecyclerViewState rvs = setupItemTouchHelper(setupRecyclerView(), END, 0);
177        rvs.mLayoutManager.expectLayouts(1);
178        setRecyclerView(rvs.mWrappedRecyclerView);
179        rvs.mLayoutManager.waitForLayout(1);
180
181        final RecyclerView.ViewHolder target = mRecyclerView
182                .findViewHolderForAdapterPosition(1);
183        target.itemView.setOnLongClickListener(new View.OnLongClickListener() {
184            @Override
185            public boolean onLongClick(View v) {
186                mItemTouchHelper.attachToRecyclerView(rvs2.mWrappedRecyclerView);
187                return false;
188            }
189        });
190        TouchUtils.longClickView(getInstrumentation(), target.itemView);
191        assertEquals(0, mCalback.mHasDragFlag.size());
192    }
193
194    public void basicSwipeTest(int dir, int swipeDirs, int targetX) throws Throwable {
195        final RecyclerViewState rvs = setupItemTouchHelper(setupRecyclerView(), 0, swipeDirs);
196        rvs.mLayoutManager.expectLayouts(1);
197        setRecyclerView(rvs.mWrappedRecyclerView);
198        rvs.mLayoutManager.waitForLayout(1);
199
200        final RecyclerView.ViewHolder target = mRecyclerView
201                .findViewHolderForAdapterPosition(1);
202        TouchUtils.dragViewToX(getInstrumentation(), target.itemView, Gravity.CENTER, targetX);
203
204        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
205            @Override
206            public boolean canProceed() {
207                return mCalback.getSwipe(target) != null;
208            }
209        });
210        final SwipeRecord swipe = mCalback.getSwipe(target);
211        assertNotNull(swipe);
212        assertEquals(dir, swipe.dir);
213        assertEquals(1, mItemTouchHelper.mRecoverAnimations.size());
214        assertEquals(1, mItemTouchHelper.mPendingCleanup.size());
215        // get rid of the view
216        rvs.mLayoutManager.expectLayouts(1);
217        rvs.mAdapter.deleteAndNotify(1, 1);
218        rvs.mLayoutManager.waitForLayout(1);
219        waitForAnimations();
220        assertEquals(0, mItemTouchHelper.mRecoverAnimations.size());
221        assertEquals(0, mItemTouchHelper.mPendingCleanup.size());
222        assertTrue(mCalback.isCleared(target));
223    }
224
225    private void waitForAnimations() throws InterruptedException {
226        while (mRecyclerView.getItemAnimator().isRunning()) {
227            Thread.sleep(100);
228        }
229    }
230
231    private static class LoggingCalback extends SimpleCallback {
232
233        private List<MoveRecord> mMoveRecordList = new ArrayList<MoveRecord>();
234
235        private List<SwipeRecord> mSwipeRecords = new ArrayList<SwipeRecord>();
236
237        private List<RecyclerView.ViewHolder> mCleared = new ArrayList<RecyclerView.ViewHolder>();
238
239        public List<Pair<RecyclerView, RecyclerView.ViewHolder>> mHasDragFlag = new ArrayList<>();
240
241        LoggingCalback(int dragDirs, int swipeDirs) {
242            super(dragDirs, swipeDirs);
243        }
244
245        @Override
246        public boolean onMove(@NonNull RecyclerView recyclerView,
247                @NonNull RecyclerView.ViewHolder viewHolder,
248                @NonNull RecyclerView.ViewHolder target) {
249            mMoveRecordList.add(new MoveRecord(viewHolder, target));
250            return true;
251        }
252
253        @Override
254        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
255            mSwipeRecords.add(new SwipeRecord(viewHolder, direction));
256        }
257
258        public MoveRecord getMove(RecyclerView.ViewHolder vh) {
259            for (MoveRecord move : mMoveRecordList) {
260                if (move.from == vh) {
261                    return move;
262                }
263            }
264            return null;
265        }
266
267        @Override
268        public void clearView(@NonNull RecyclerView recyclerView,
269                @NonNull RecyclerView.ViewHolder viewHolder) {
270            super.clearView(recyclerView, viewHolder);
271            mCleared.add(viewHolder);
272        }
273
274        @Override
275        boolean hasDragFlag(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
276            mHasDragFlag.add(new Pair<>(recyclerView, viewHolder));
277            return super.hasDragFlag(recyclerView, viewHolder);
278        }
279
280        public SwipeRecord getSwipe(RecyclerView.ViewHolder vh) {
281            for (SwipeRecord swipe : mSwipeRecords) {
282                if (swipe.viewHolder == vh) {
283                    return swipe;
284                }
285            }
286            return null;
287        }
288
289        public boolean isCleared(RecyclerView.ViewHolder vh) {
290            return mCleared.contains(vh);
291        }
292    }
293
294    private static class LoggingItemTouchHelper extends ItemTouchHelper {
295
296        public LoggingItemTouchHelper(Callback callback) {
297            super(callback);
298        }
299    }
300
301    private static class SwipeRecord {
302
303        RecyclerView.ViewHolder viewHolder;
304
305        int dir;
306
307        public SwipeRecord(RecyclerView.ViewHolder viewHolder, int dir) {
308            this.viewHolder = viewHolder;
309            this.dir = dir;
310        }
311    }
312
313    private static class MoveRecord {
314
315        final int fromPos, toPos;
316
317        RecyclerView.ViewHolder from, to;
318
319        MoveRecord(RecyclerView.ViewHolder from, RecyclerView.ViewHolder to) {
320            this.from = from;
321            this.to = to;
322            fromPos = from.getAdapterPosition();
323            toPos = to.getAdapterPosition();
324        }
325    }
326}
327