1/*
2 * Copyright (C) 2016 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.core.view;
18
19import static org.mockito.Matchers.any;
20import static org.mockito.Matchers.argThat;
21import static org.mockito.Matchers.eq;
22import static org.mockito.Mockito.inOrder;
23import static org.mockito.Mockito.mock;
24import static org.mockito.Mockito.times;
25import static org.mockito.Mockito.verify;
26import static org.mockito.Mockito.verifyNoMoreInteractions;
27import static org.mockito.Mockito.when;
28
29import android.app.Instrumentation;
30import android.graphics.Point;
31import android.os.SystemClock;
32import android.support.test.InstrumentationRegistry;
33import android.support.test.filters.LargeTest;
34import android.support.test.filters.SmallTest;
35import android.support.test.rule.ActivityTestRule;
36import android.support.test.runner.AndroidJUnit4;
37import android.view.InputDevice;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.ViewConfiguration;
41
42import androidx.annotation.NonNull;
43import androidx.core.test.R;
44
45import org.junit.Before;
46import org.junit.Rule;
47import org.junit.Test;
48import org.junit.runner.RunWith;
49import org.mockito.ArgumentMatcher;
50import org.mockito.InOrder;
51
52@RunWith(AndroidJUnit4.class)
53public class DragStartHelperTest {
54
55    @Rule
56    public ActivityTestRule<DragStartHelperTestActivity> mActivityRule =
57            new ActivityTestRule<>(DragStartHelperTestActivity.class);
58
59    private Instrumentation mInstrumentation;
60    private View mDragSource;
61
62    interface DragStartListener {
63        boolean onDragStart(View view, DragStartHelper helper, Point touchPosition);
64    }
65
66    @NonNull
67    private DragStartListener createListener(boolean returnValue) {
68        final DragStartListener listener = mock(DragStartListener.class);
69        when(listener.onDragStart(any(View.class), any(DragStartHelper.class), any(Point.class)))
70                .thenReturn(returnValue);
71        return listener;
72    }
73
74    @NonNull
75    private DragStartHelper createDragStartHelper(final DragStartListener listener) {
76        return new DragStartHelper(mDragSource, new DragStartHelper.OnDragStartListener() {
77            @Override
78            public boolean onDragStart(View v, DragStartHelper helper) {
79                Point touchPosition = new Point();
80                helper.getTouchPosition(touchPosition);
81                return listener.onDragStart(v, helper, touchPosition);
82            }
83        });
84    }
85
86    private static int[] getViewCenter(View view) {
87        final int[] xy = new int[2];
88        view.getLocationOnScreen(xy);
89        xy[0] += view.getWidth() / 2;
90        xy[1] += view.getHeight() / 2;
91        return xy;
92    }
93
94    private static MotionEvent obtainTouchEvent(
95            int action, View anchor, int offsetX, int offsetY) {
96        final long eventTime = SystemClock.uptimeMillis();
97        final int[] xy = getViewCenter(anchor);
98        return MotionEvent.obtain(
99                eventTime, eventTime, action, xy[0] + offsetX, xy[1] + offsetY, 0);
100    }
101
102    private void sendTouchEvent(int action, View anchor, int offsetX, int offsetY) {
103        mInstrumentation.sendPointerSync(obtainTouchEvent(action, anchor, offsetX, offsetY));
104    }
105
106    private static MotionEvent obtainMouseEvent(
107            int action, int buttonState, View anchor, int offsetX, int offsetY) {
108        final long eventTime = SystemClock.uptimeMillis();
109
110        final int[] xy = getViewCenter(anchor);
111
112        MotionEvent.PointerProperties[] props = new MotionEvent.PointerProperties[] {
113                new MotionEvent.PointerProperties()
114        };
115        props[0].id = 0;
116        props[0].toolType = MotionEvent.TOOL_TYPE_MOUSE;
117
118        MotionEvent.PointerCoords[] coords = new MotionEvent.PointerCoords[] {
119                new MotionEvent.PointerCoords()
120        };
121        coords[0].x = xy[0] + offsetX;
122        coords[0].y = xy[1] + offsetY;
123
124        return MotionEvent.obtain(eventTime, eventTime, action, 1, props, coords, 0,
125                buttonState, 0, 0, -1, 0, InputDevice.SOURCE_MOUSE, 0);
126    }
127
128    private void sendMouseEvent(
129            int action, int buttonState, View anchor, int offsetX, int offsetY) {
130        mInstrumentation.sendPointerSync(obtainMouseEvent(
131                action, buttonState, anchor, offsetX, offsetY));
132    }
133
134    static class TouchPositionMatcher implements ArgumentMatcher<Point> {
135
136        private final Point mExpectedPosition;
137
138        TouchPositionMatcher(int x, int y) {
139            mExpectedPosition = new Point(x, y);
140        }
141
142        TouchPositionMatcher(View anchor, int x, int y) {
143            this(anchor.getWidth() / 2 + x, anchor.getHeight() / 2 + y);
144        }
145
146        @Override
147        public boolean matches(Point actual) {
148            return mExpectedPosition.equals(actual);
149        }
150
151        @Override
152        public String toString() {
153            return "TouchPositionMatcher: " + mExpectedPosition;
154        }
155    }
156
157    private void waitForLongPress() {
158        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() * 2);
159    }
160
161    @Before
162    public void setUp() {
163        mInstrumentation = InstrumentationRegistry.getInstrumentation();
164        mDragSource = mActivityRule.getActivity().findViewById(R.id.drag_source);
165    }
166
167    @SmallTest
168    @Test
169    public void mouseClick() throws Throwable {
170        final DragStartListener listener = createListener(true);
171        final DragStartHelper helper = createDragStartHelper(listener);
172        helper.attach();
173
174        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
175        sendMouseEvent(MotionEvent.ACTION_UP, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
176
177        // A simple mouse click does not trigger OnDragStart.
178        verifyNoMoreInteractions(listener);
179    }
180
181    @SmallTest
182    @Test
183    public void mousePressWithSecondaryButton() throws Throwable {
184        final DragStartListener listener = createListener(true);
185        final DragStartHelper helper = createDragStartHelper(listener);
186        helper.attach();
187
188        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
189        sendMouseEvent(MotionEvent.ACTION_MOVE,
190                MotionEvent.BUTTON_PRIMARY | MotionEvent.BUTTON_SECONDARY, mDragSource, 0, 0);
191        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
192
193        // ACTION_MOVE with the same position does not trigger OnDragStart.
194        verifyNoMoreInteractions(listener);
195    }
196
197    @SmallTest
198    @Test
199    public void mouseDrag() throws Throwable {
200        final DragStartListener listener = createListener(true);
201        final DragStartHelper helper = createDragStartHelper(listener);
202        helper.attach();
203
204        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
205        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 1, 2);
206        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 3, 4);
207        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 5, 6);
208
209        // Returning true from the callback prevents further callbacks.
210        verify(listener, times(1)).onDragStart(
211                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 1, 2)));
212        verifyNoMoreInteractions(listener);
213    }
214
215    @SmallTest
216    @Test
217    public void mouseDragWithNonprimaryButton() throws Throwable {
218        final DragStartListener listener = createListener(true);
219        final DragStartHelper helper = createDragStartHelper(listener);
220        helper.attach();
221
222        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_SECONDARY, mDragSource, 0, 0);
223        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_SECONDARY, mDragSource, 1, 2);
224        sendMouseEvent(MotionEvent.ACTION_UP, MotionEvent.BUTTON_SECONDARY, mDragSource, 3, 4);
225
226        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_TERTIARY, mDragSource, 0, 0);
227        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_TERTIARY, mDragSource, 1, 2);
228        sendMouseEvent(MotionEvent.ACTION_UP, MotionEvent.BUTTON_TERTIARY, mDragSource, 3, 4);
229
230        // Dragging mouse with a non-primary button down does not trigger OnDragStart.
231        verifyNoMoreInteractions(listener);
232    }
233
234    @SmallTest
235    @Test
236    public void mouseDragUsingTouchListener() throws Throwable {
237        final DragStartListener listener = createListener(true);
238        final DragStartHelper helper = createDragStartHelper(listener);
239
240        mDragSource.setOnTouchListener(new View.OnTouchListener() {
241            @Override
242            public boolean onTouch(View view, MotionEvent motionEvent) {
243                helper.onTouch(view, motionEvent);
244                return true;
245            }
246        });
247
248        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
249        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 1, 2);
250        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 3, 4);
251        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 5, 6);
252
253        // Returning true from the callback prevents further callbacks.
254        verify(listener, times(1)).onDragStart(
255                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 1, 2)));
256        verifyNoMoreInteractions(listener);
257    }
258
259    @SmallTest
260    @Test
261    public void mouseDragWhenListenerReturnsFalse() throws Throwable {
262        final DragStartListener listener = createListener(false);
263        final DragStartHelper helper = createDragStartHelper(listener);
264        helper.attach();
265
266        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_PRIMARY, mDragSource, 0, 0);
267        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 1, 2);
268        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 3, 4);
269        sendMouseEvent(MotionEvent.ACTION_MOVE, MotionEvent.BUTTON_PRIMARY, mDragSource, 5, 6);
270
271        // When the listener returns false every ACTION_MOVE triggers OnDragStart.
272        InOrder inOrder = inOrder(listener);
273        inOrder.verify(listener, times(1)).onDragStart(
274                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 1, 2)));
275        inOrder.verify(listener, times(1)).onDragStart(
276                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 3, 4)));
277        inOrder.verify(listener, times(1)).onDragStart(
278                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 5, 6)));
279        inOrder.verifyNoMoreInteractions();
280    }
281
282    @LargeTest
283    @Test
284    public void mouseLongPress() throws Throwable {
285        final DragStartListener listener = createListener(true);
286        final DragStartHelper helper = createDragStartHelper(listener);
287        helper.attach();
288
289        sendMouseEvent(MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_PRIMARY, mDragSource, 1, 2);
290        waitForLongPress();
291
292        // Long press triggers OnDragStart.
293        verify(listener, times(1)).onDragStart(
294                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 1, 2)));
295        verifyNoMoreInteractions(listener);
296    }
297
298    @SmallTest
299    @Test
300    public void touchDrag() throws Throwable {
301        final DragStartListener listener = createListener(false);
302        final DragStartHelper helper = createDragStartHelper(listener);
303        helper.attach();
304
305        sendTouchEvent(MotionEvent.ACTION_DOWN, mDragSource, 0, 0);
306        sendTouchEvent(MotionEvent.ACTION_MOVE, mDragSource, 1, 2);
307        sendTouchEvent(MotionEvent.ACTION_MOVE, mDragSource, 3, 4);
308        sendTouchEvent(MotionEvent.ACTION_MOVE, mDragSource, 5, 6);
309
310        // Touch and drag (without delay) does not trigger OnDragStart.
311        verifyNoMoreInteractions(listener);
312    }
313
314    @SmallTest
315    @Test
316    public void touchTap() throws Throwable {
317        final DragStartListener listener = createListener(false);
318        final DragStartHelper helper = createDragStartHelper(listener);
319        helper.attach();
320
321        sendTouchEvent(MotionEvent.ACTION_DOWN, mDragSource, 0, 0);
322        sendTouchEvent(MotionEvent.ACTION_UP, mDragSource, 0, 0);
323
324        // A simple tap does not trigger OnDragStart.
325        verifyNoMoreInteractions(listener);
326    }
327
328    @LargeTest
329    @Test
330    public void touchLongPress() throws Throwable {
331        final DragStartListener listener = createListener(true);
332        final DragStartHelper helper = createDragStartHelper(listener);
333        helper.attach();
334
335        sendTouchEvent(MotionEvent.ACTION_DOWN, mDragSource, 1, 2);
336        waitForLongPress();
337
338        // Long press triggers OnDragStart.
339        verify(listener, times(1)).onDragStart(
340                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(mDragSource, 1, 2)));
341        verifyNoMoreInteractions(listener);
342    }
343
344    @LargeTest
345    @Test
346    public void touchLongPressUsingLongClickListener() throws Throwable {
347        final DragStartListener listener = createListener(true);
348
349        final DragStartHelper helper = createDragStartHelper(listener);
350        mDragSource.setOnLongClickListener(new View.OnLongClickListener() {
351            @Override
352            public boolean onLongClick(View view) {
353                return helper.onLongClick(view);
354            }
355        });
356
357        sendTouchEvent(MotionEvent.ACTION_DOWN, mDragSource, 1, 2);
358        waitForLongPress();
359
360        // Long press triggers OnDragStart.
361        // Since ACTION_DOWN is not handled, the touch offset is not available.
362        verify(listener, times(1)).onDragStart(
363                eq(mDragSource), eq(helper), argThat(new TouchPositionMatcher(0, 0)));
364        verifyNoMoreInteractions(listener);
365    }
366}
367