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 */ compareParcelables(Parcelable obj1, Parcelable obj2)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 LogEntry(View v, int action, float x, float y, ClipData clipData, ClipDescription clipDescription, Object localState, boolean result)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 equals(Object obj)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 toString()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 obtainClipData(int action)157 private static ClipData obtainClipData(int action) { 158 if (action == DragEvent.ACTION_DROP) { 159 return sClipData; 160 } 161 return null; 162 } 163 obtainClipDescription(int action)164 private static ClipDescription obtainClipDescription(int action) { 165 if (action == DragEvent.ACTION_DRAG_ENDED) { 166 return null; 167 } 168 return sClipDescription; 169 } 170 logEvent(View v, DragEvent ev)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. expectEvent5(int action, int viewId)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. expectEndEvent(int viewId, float x, float y, boolean result)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. expectEndEventSuccess(int viewId)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. expectEndEventFailure6(int viewId, int 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. expectEventWithOffset(int action, int viewId, int locationViewId, int offset)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 expectEvent5(int action, int viewId, int locationViewId)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. expectEvent6(int action, int viewId, int locationViewId)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. injectMouseWithOffset(int viewId, int action, int offset)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. injectMouse5(int viewId, int action)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. injectMouse6(int viewId, int action)270 private void injectMouse6(int viewId, int action) { 271 injectMouseWithOffset(viewId, action, 6); 272 } 273 logToString(ArrayList<LogEntry> log)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 failWithLogs(String message)283 private void failWithLogs(String message) { 284 fail(message + ":\nExpected event sequence:\n" + logToString(mExpected) + 285 "\nActual event sequence:\n" + logToString(mActual)); 286 } 287 verifyEventLog()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 init()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 setUp()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 tearDown()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. setRejectingHandlersOnTree(View v)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 runOnMain(Runnable runner)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 startDrag()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 testNoExtraEvents()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 testBlackHole()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 testEnterExit()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 testOverNowhere()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 testAcceptingGroupInTheMiddle()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 drawableStateContains(int resourceId, int attr)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 testDrawableState()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 }