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