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