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