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