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