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