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