Home | History | Annotate | Download | only in view
      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 
     17 package androidx.core.view;
     18 
     19 import static org.mockito.Matchers.any;
     20 import static org.mockito.Matchers.argThat;
     21 import static org.mockito.Matchers.eq;
     22 import static org.mockito.Mockito.inOrder;
     23 import static org.mockito.Mockito.mock;
     24 import static org.mockito.Mockito.times;
     25 import static org.mockito.Mockito.verify;
     26 import static org.mockito.Mockito.verifyNoMoreInteractions;
     27 import static org.mockito.Mockito.when;
     28 
     29 import android.app.Instrumentation;
     30 import android.graphics.Point;
     31 import android.os.SystemClock;
     32 import android.support.test.InstrumentationRegistry;
     33 import android.support.test.filters.LargeTest;
     34 import android.support.test.filters.SmallTest;
     35 import android.support.test.rule.ActivityTestRule;
     36 import android.support.test.runner.AndroidJUnit4;
     37 import android.view.InputDevice;
     38 import android.view.MotionEvent;
     39 import android.view.View;
     40 import android.view.ViewConfiguration;
     41 
     42 import androidx.annotation.NonNull;
     43 import androidx.core.test.R;
     44 
     45 import org.junit.Before;
     46 import org.junit.Rule;
     47 import org.junit.Test;
     48 import org.junit.runner.RunWith;
     49 import org.mockito.ArgumentMatcher;
     50 import org.mockito.InOrder;
     51 
     52 @RunWith(AndroidJUnit4.class)
     53 public 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