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