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 android.view.cts; 18 19 import static junit.framework.TestCase.assertFalse; 20 import static junit.framework.TestCase.assertTrue; 21 import static junit.framework.TestCase.fail; 22 23 import android.app.Instrumentation; 24 import android.app.UiAutomation; 25 import android.content.ClipData; 26 import android.content.ClipDescription; 27 import android.content.pm.PackageManager; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.os.SystemClock; 31 import android.support.test.InstrumentationRegistry; 32 import android.support.test.rule.ActivityTestRule; 33 import android.support.test.runner.AndroidJUnit4; 34 import android.view.DragEvent; 35 import android.view.InputDevice; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewGroup; 39 40 import org.junit.After; 41 import org.junit.Before; 42 import org.junit.Rule; 43 import org.junit.Test; 44 import org.junit.runner.RunWith; 45 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.TimeUnit; 50 import java.util.stream.IntStream; 51 52 @RunWith(AndroidJUnit4.class) 53 public class DragDropTest { 54 static final String TAG = "DragDropTest"; 55 56 final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); 57 final UiAutomation mAutomation = mInstrumentation.getUiAutomation(); 58 59 @Rule 60 public ActivityTestRule<DragDropActivity> mActivityRule = 61 new ActivityTestRule<>(DragDropActivity.class); 62 63 private DragDropActivity mActivity; 64 65 private CountDownLatch mStartReceived; 66 private CountDownLatch mEndReceived; 67 68 private AssertionError mMainThreadAssertionError; 69 70 /** 71 * Check whether two objects have the same binary data when dumped into Parcels 72 * @return True if the objects are equal 73 */ compareParcelables(Parcelable obj1, Parcelable obj2)74 private static boolean compareParcelables(Parcelable obj1, Parcelable obj2) { 75 if (obj1 == null && obj2 == null) { 76 return true; 77 } 78 if (obj1 == null || obj2 == null) { 79 return false; 80 } 81 Parcel p1 = Parcel.obtain(); 82 obj1.writeToParcel(p1, 0); 83 Parcel p2 = Parcel.obtain(); 84 obj2.writeToParcel(p2, 0); 85 boolean result = Arrays.equals(p1.marshall(), p2.marshall()); 86 p1.recycle(); 87 p2.recycle(); 88 return result; 89 } 90 91 private static final ClipDescription sClipDescription = 92 new ClipDescription("TestLabel", new String[]{"text/plain"}); 93 private static final ClipData sClipData = 94 new ClipData(sClipDescription, new ClipData.Item("TestText")); 95 private static final Object sLocalState = new Object(); // just check if null or not 96 97 class LogEntry { 98 public View view; 99 100 // Public DragEvent fields 101 public int action; // DragEvent.getAction() 102 public float x; // DragEvent.getX() 103 public float y; // DragEvent.getY() 104 public ClipData clipData; // DragEvent.getClipData() 105 public ClipDescription clipDescription; // DragEvent.getClipDescription() 106 public Object localState; // DragEvent.getLocalState() 107 public boolean result; // DragEvent.getResult() 108 LogEntry(View v, int action, float x, float y, ClipData clipData, ClipDescription clipDescription, Object localState, boolean result)109 LogEntry(View v, int action, float x, float y, ClipData clipData, 110 ClipDescription clipDescription, Object localState, boolean result) { 111 this.view = v; 112 this.action = action; 113 this.x = x; 114 this.y = y; 115 this.clipData = clipData; 116 this.clipDescription = clipDescription; 117 this.localState = localState; 118 this.result = result; 119 } 120 121 @Override equals(Object obj)122 public boolean equals(Object obj) { 123 if (this == obj) { 124 return true; 125 } 126 if (!(obj instanceof LogEntry)) { 127 return false; 128 } 129 final LogEntry other = (LogEntry) obj; 130 return view == other.view && action == other.action 131 && x == other.x && y == other.y 132 && compareParcelables(clipData, other.clipData) 133 && compareParcelables(clipDescription, other.clipDescription) 134 && localState == other.localState 135 && result == other.result; 136 } 137 138 @Override toString()139 public String toString() { 140 StringBuilder sb = new StringBuilder(); 141 sb.append("DragEvent {action=").append(action).append(" x=").append(x).append(" y=") 142 .append(y).append(" result=").append(result).append("}") 143 .append(" @ ").append(view); 144 return sb.toString(); 145 } 146 } 147 148 // Actual and expected sequences of events. 149 // While the test is running, logs should be accessed only from the main thread. 150 final private ArrayList<LogEntry> mActual = new ArrayList<LogEntry> (); 151 final private ArrayList<LogEntry> mExpected = new ArrayList<LogEntry> (); 152 obtainClipData(int action)153 private static ClipData obtainClipData(int action) { 154 if (action == DragEvent.ACTION_DROP) { 155 return sClipData; 156 } 157 return null; 158 } 159 obtainClipDescription(int action)160 private static ClipDescription obtainClipDescription(int action) { 161 if (action == DragEvent.ACTION_DRAG_ENDED) { 162 return null; 163 } 164 return sClipDescription; 165 } 166 logEvent(View v, DragEvent ev)167 private void logEvent(View v, DragEvent ev) { 168 if (ev.getAction() == DragEvent.ACTION_DRAG_STARTED) { 169 mStartReceived.countDown(); 170 } 171 if (ev.getAction() == DragEvent.ACTION_DRAG_ENDED) { 172 mEndReceived.countDown(); 173 } 174 mActual.add(new LogEntry(v, ev.getAction(), ev.getX(), ev.getY(), ev.getClipData(), 175 ev.getClipDescription(), ev.getLocalState(), ev.getResult())); 176 } 177 178 // Add expected event for a view, with zero coordinates. expectEvent5(int action, int viewId)179 private void expectEvent5(int action, int viewId) { 180 View v = mActivity.findViewById(viewId); 181 mExpected.add(new LogEntry(v, action, 0, 0, obtainClipData(action), 182 obtainClipDescription(action), sLocalState, false)); 183 } 184 185 // Add expected event for a view. expectEndEvent(int viewId, float x, float y, boolean result)186 private void expectEndEvent(int viewId, float x, float y, boolean result) { 187 View v = mActivity.findViewById(viewId); 188 int action = DragEvent.ACTION_DRAG_ENDED; 189 mExpected.add(new LogEntry(v, action, x, y, obtainClipData(action), 190 obtainClipDescription(action), sLocalState, result)); 191 } 192 193 // Add expected successful-end event for a view. expectEndEventSuccess(int viewId)194 private void expectEndEventSuccess(int viewId) { 195 expectEndEvent(viewId, 0, 0, true); 196 } 197 198 // Add expected failed-end event for a view, with the release coordinates shifted by 6 relative 199 // to the left-upper corner of a view with id releaseViewId. expectEndEventFailure6(int viewId, int releaseViewId)200 private void expectEndEventFailure6(int viewId, int releaseViewId) { 201 View v = mActivity.findViewById(viewId); 202 View release = mActivity.findViewById(releaseViewId); 203 int [] releaseLoc = new int[2]; 204 release.getLocationOnScreen(releaseLoc); 205 int action = DragEvent.ACTION_DRAG_ENDED; 206 mExpected.add(new LogEntry(v, action, 207 releaseLoc[0] + 6, releaseLoc[1] + 6, obtainClipData(action), 208 obtainClipDescription(action), sLocalState, false)); 209 } 210 211 // Add expected event for a view, with coordinates over view locationViewId, with the specified 212 // offset from the location view's upper-left corner. expectEventWithOffset(int action, int viewId, int locationViewId, int offset)213 private void expectEventWithOffset(int action, int viewId, int locationViewId, int offset) { 214 View v = mActivity.findViewById(viewId); 215 View locationView = mActivity.findViewById(locationViewId); 216 int [] viewLocation = new int[2]; 217 v.getLocationOnScreen(viewLocation); 218 int [] locationViewLocation = new int[2]; 219 locationView.getLocationOnScreen(locationViewLocation); 220 mExpected.add(new LogEntry(v, action, 221 locationViewLocation[0] - viewLocation[0] + offset, 222 locationViewLocation[1] - viewLocation[1] + offset, obtainClipData(action), 223 obtainClipDescription(action), sLocalState, false)); 224 } 225 expectEvent5(int action, int viewId, int locationViewId)226 private void expectEvent5(int action, int viewId, int locationViewId) { 227 expectEventWithOffset(action, viewId, locationViewId, 5); 228 } 229 230 // See comment for injectMouse6 on why we need both *5 and *6 methods. expectEvent6(int action, int viewId, int locationViewId)231 private void expectEvent6(int action, int viewId, int locationViewId) { 232 expectEventWithOffset(action, viewId, locationViewId, 6); 233 } 234 235 // Inject mouse event over a given view, with specified offset from its left-upper corner. injectMouseWithOffset(int viewId, int action, int offset)236 private void injectMouseWithOffset(int viewId, int action, int offset) { 237 runOnMain(() -> { 238 View v = mActivity.findViewById(viewId); 239 int [] destLoc = new int [2]; 240 v.getLocationOnScreen(destLoc); 241 long downTime = SystemClock.uptimeMillis(); 242 MotionEvent event = MotionEvent.obtain(downTime, downTime, action, 243 destLoc[0] + offset, destLoc[1] + offset, 1); 244 event.setSource(InputDevice.SOURCE_TOUCHSCREEN); 245 mAutomation.injectInputEvent(event, false); 246 }); 247 248 // Wait till the mouse event generates drag events. Also, some waiting needed because the 249 // system seems to collapse too frequent mouse events. 250 try { 251 Thread.sleep(100); 252 } catch (Exception e) { 253 fail("Exception while wait: " + e); 254 } 255 } 256 257 // Inject mouse event over a given view, with offset 5 from its left-upper corner. injectMouse5(int viewId, int action)258 private void injectMouse5(int viewId, int action) { 259 injectMouseWithOffset(viewId, action, 5); 260 } 261 262 // Inject mouse event over a given view, with offset 6 from its left-upper corner. 263 // We need both injectMouse5 and injectMouse6 if we want to inject 2 events in a row in the same 264 // view, and want them to produce distinct drag events or simply drag events with different 265 // coordinates. injectMouse6(int viewId, int action)266 private void injectMouse6(int viewId, int action) { 267 injectMouseWithOffset(viewId, action, 6); 268 } 269 logToString(ArrayList<LogEntry> log)270 private String logToString(ArrayList<LogEntry> log) { 271 StringBuilder sb = new StringBuilder(); 272 for (int i = 0; i < log.size(); ++i) { 273 LogEntry e = log.get(i); 274 sb.append("#").append(i + 1).append(": ").append(e).append('\n'); 275 } 276 return sb.toString(); 277 } 278 failWithLogs(String message)279 private void failWithLogs(String message) { 280 fail(message + ":\nExpected event sequence:\n" + logToString(mExpected) + 281 "\nActual event sequence:\n" + logToString(mActual)); 282 } 283 verifyEventLog()284 private void verifyEventLog() { 285 try { 286 assertTrue("Timeout while waiting for END event", 287 mEndReceived.await(1, TimeUnit.SECONDS)); 288 } catch (InterruptedException e) { 289 fail("Got InterruptedException while waiting for END event"); 290 } 291 292 // Verify the log. 293 runOnMain(() -> { 294 if (mExpected.size() != mActual.size()) { 295 failWithLogs("Actual log has different size than expected"); 296 } 297 298 for (int i = 0; i < mActual.size(); ++i) { 299 if (!mActual.get(i).equals(mExpected.get(i))) { 300 failWithLogs("Actual event #" + (i + 1) + " is different from expected"); 301 } 302 } 303 }); 304 } 305 init()306 private boolean init() { 307 // Only run for non-watch devices 308 if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { 309 return false; 310 } 311 return true; 312 } 313 314 @Before setUp()315 public void setUp() { 316 mActivity = mActivityRule.getActivity(); 317 mStartReceived = new CountDownLatch(1); 318 mEndReceived = new CountDownLatch(1); 319 320 // Wait for idle 321 mInstrumentation.waitForIdleSync(); 322 } 323 324 @After tearDown()325 public void tearDown() throws Exception { 326 mActual.clear(); 327 mExpected.clear(); 328 } 329 330 // Sets handlers on all views in a tree, which log the event and return false. setRejectingHandlersOnTree(View v)331 private void setRejectingHandlersOnTree(View v) { 332 v.setOnDragListener((_v, ev) -> { 333 logEvent(_v, ev); 334 return false; 335 }); 336 337 if (v instanceof ViewGroup) { 338 ViewGroup group = (ViewGroup) v; 339 for (int i = 0; i < group.getChildCount(); ++i) { 340 setRejectingHandlersOnTree(group.getChildAt(i)); 341 } 342 } 343 } 344 runOnMain(Runnable runner)345 private void runOnMain(Runnable runner) throws AssertionError { 346 mMainThreadAssertionError = null; 347 mInstrumentation.runOnMainSync(() -> { 348 try { 349 runner.run(); 350 } catch (AssertionError error) { 351 mMainThreadAssertionError = error; 352 } 353 }); 354 if (mMainThreadAssertionError != null) { 355 throw mMainThreadAssertionError; 356 } 357 } 358 startDrag()359 private void startDrag() { 360 // Mouse down. Required for the drag to start. 361 injectMouse5(R.id.draggable, MotionEvent.ACTION_DOWN); 362 363 runOnMain(() -> { 364 // Start drag. 365 View v = mActivity.findViewById(R.id.draggable); 366 assertTrue("Couldn't start drag", 367 v.startDragAndDrop(sClipData, new View.DragShadowBuilder(v), sLocalState, 0)); 368 }); 369 370 try { 371 assertTrue("Timeout while waiting for START event", 372 mStartReceived.await(1, TimeUnit.SECONDS)); 373 } catch (InterruptedException e) { 374 fail("Got InterruptedException while waiting for START event"); 375 } 376 } 377 378 /** 379 * Tests that no drag-drop events are sent to views that aren't supposed to receive them. 380 */ 381 @Test testNoExtraEvents()382 public void testNoExtraEvents() throws Exception { 383 if (!init()) { 384 return; 385 } 386 387 runOnMain(() -> { 388 // Tell all views in layout to return false to all events, and log them. 389 setRejectingHandlersOnTree(mActivity.findViewById(R.id.drag_drop_activity_main)); 390 391 // Override handlers for the inner view and its parent to return true. 392 mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { 393 logEvent(v, ev); 394 return true; 395 }); 396 mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { 397 logEvent(v, ev); 398 return true; 399 }); 400 }); 401 402 startDrag(); 403 404 // Move mouse to the outmost view. This shouldn't generate any events since it returned 405 // false to STARTED. 406 injectMouse5(R.id.container, MotionEvent.ACTION_MOVE); 407 // Release mouse over the inner view. This produces DROP there. 408 injectMouse5(R.id.inner, MotionEvent.ACTION_UP); 409 410 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); 411 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); 412 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); 413 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.draggable, R.id.draggable); 414 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.drag_drop_activity_main, R.id.draggable); 415 416 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); 417 expectEvent5(DragEvent.ACTION_DROP, R.id.inner, R.id.inner); 418 419 expectEndEventSuccess(R.id.inner); 420 expectEndEventSuccess(R.id.subcontainer); 421 422 verifyEventLog(); 423 } 424 425 /** 426 * Tests events over a non-accepting view with an accepting child get delivered to that view's 427 * parent. 428 */ 429 @Test testBlackHole()430 public void testBlackHole() throws Exception { 431 if (!init()) { 432 return; 433 } 434 435 runOnMain(() -> { 436 // Accepting child. 437 mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { 438 logEvent(v, ev); 439 return true; 440 }); 441 // Non-accepting parent of that child. 442 mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { 443 logEvent(v, ev); 444 return false; 445 }); 446 // Accepting parent of the previous view. 447 mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> { 448 logEvent(v, ev); 449 return true; 450 }); 451 }); 452 453 startDrag(); 454 455 // Move mouse to the non-accepting view. 456 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 457 // Release mouse over the non-accepting view, with different coordinates. 458 injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP); 459 460 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); 461 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); 462 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); 463 464 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); 465 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer); 466 expectEvent6(DragEvent.ACTION_DROP, R.id.container, R.id.subcontainer); 467 468 expectEndEventSuccess(R.id.inner); 469 expectEndEventSuccess(R.id.container); 470 471 verifyEventLog(); 472 } 473 474 /** 475 * Tests generation of ENTER/EXIT events. 476 */ 477 @Test testEnterExit()478 public void testEnterExit() throws Exception { 479 if (!init()) { 480 return; 481 } 482 483 runOnMain(() -> { 484 // The setup is same as for testBlackHole. 485 486 // Accepting child. 487 mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { 488 logEvent(v, ev); 489 return true; 490 }); 491 // Non-accepting parent of that child. 492 mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { 493 logEvent(v, ev); 494 return false; 495 }); 496 // Accepting parent of the previous view. 497 mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> { 498 logEvent(v, ev); 499 return true; 500 }); 501 502 }); 503 504 startDrag(); 505 506 // Move mouse to the non-accepting view, then to the inner one, then back to the 507 // non-accepting view, then release over the inner. 508 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 509 injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); 510 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 511 injectMouse5(R.id.inner, MotionEvent.ACTION_UP); 512 513 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); 514 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); 515 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); 516 517 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); 518 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer); 519 expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container); 520 521 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); 522 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.inner, R.id.inner); 523 expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.inner); 524 525 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); 526 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer); 527 expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container); 528 529 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); 530 expectEvent5(DragEvent.ACTION_DROP, R.id.inner, R.id.inner); 531 532 expectEndEventSuccess(R.id.inner); 533 expectEndEventSuccess(R.id.container); 534 535 verifyEventLog(); 536 } 537 /** 538 * Tests events over a non-accepting view that has no accepting ancestors. 539 */ 540 @Test testOverNowhere()541 public void testOverNowhere() throws Exception { 542 if (!init()) { 543 return; 544 } 545 546 runOnMain(() -> { 547 // Accepting child. 548 mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { 549 logEvent(v, ev); 550 return true; 551 }); 552 // Non-accepting parent of that child. 553 mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { 554 logEvent(v, ev); 555 return false; 556 }); 557 }); 558 559 startDrag(); 560 561 // Move mouse to the non-accepting view, then to accepting view, and back, and drop there. 562 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 563 injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); 564 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 565 injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP); 566 567 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); 568 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); 569 570 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); 571 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.inner, R.id.inner); 572 expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.inner); 573 574 expectEndEventFailure6(R.id.inner, R.id.subcontainer); 575 576 verifyEventLog(); 577 } 578 579 /** 580 * Tests that events are properly delivered to a view that is in the middle of the accepting 581 * hierarchy. 582 */ 583 @Test testAcceptingGroupInTheMiddle()584 public void testAcceptingGroupInTheMiddle() throws Exception { 585 if (!init()) { 586 return; 587 } 588 589 runOnMain(() -> { 590 // Set accepting handlers to the inner view and its 2 ancestors. 591 mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { 592 logEvent(v, ev); 593 return true; 594 }); 595 mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { 596 logEvent(v, ev); 597 return true; 598 }); 599 mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> { 600 logEvent(v, ev); 601 return true; 602 }); 603 }); 604 605 startDrag(); 606 607 // Move mouse to the outmost container, then move to the subcontainer and drop there. 608 injectMouse5(R.id.container, MotionEvent.ACTION_MOVE); 609 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 610 injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP); 611 612 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); 613 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); 614 expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); 615 616 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); 617 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.container); 618 expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container); 619 620 expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.subcontainer); 621 expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.subcontainer, R.id.subcontainer); 622 expectEvent6(DragEvent.ACTION_DROP, R.id.subcontainer, R.id.subcontainer); 623 624 expectEndEventSuccess(R.id.inner); 625 expectEndEventSuccess(R.id.subcontainer); 626 expectEndEventSuccess(R.id.container); 627 628 verifyEventLog(); 629 } 630 drawableStateContains(int resourceId, int attr)631 private boolean drawableStateContains(int resourceId, int attr) { 632 return IntStream.of(mActivity.findViewById(resourceId).getDrawableState()) 633 .anyMatch(x -> x == attr); 634 } 635 636 /** 637 * Tests that state_drag_hovered and state_drag_can_accept are set correctly. 638 */ 639 @Test testDrawableState()640 public void testDrawableState() throws Exception { 641 if (!init()) { 642 return; 643 } 644 645 runOnMain(() -> { 646 // Set accepting handler for the inner view. 647 mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { 648 logEvent(v, ev); 649 return true; 650 }); 651 assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_can_accept)); 652 }); 653 654 startDrag(); 655 656 runOnMain(() -> { 657 assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered)); 658 assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_can_accept)); 659 }); 660 661 // Move mouse into the view. 662 injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); 663 runOnMain(() -> { 664 assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered)); 665 }); 666 667 // Move out. 668 injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); 669 runOnMain(() -> { 670 assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered)); 671 }); 672 673 // Move in. 674 injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); 675 runOnMain(() -> { 676 assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered)); 677 }); 678 679 // Release there. 680 injectMouse5(R.id.inner, MotionEvent.ACTION_UP); 681 runOnMain(() -> { 682 assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered)); 683 }); 684 } 685 }