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