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