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