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 
21 import static org.junit.Assert.assertFalse;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import android.app.Instrumentation;
26 import android.app.UiAutomation;
27 import android.content.ClipData;
28 import android.content.ClipDescription;
29 import android.content.pm.PackageManager;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.os.SystemClock;
33 import android.server.wm.cts.R;
34 import android.view.DragEvent;
35 import android.view.InputDevice;
36 import android.view.MotionEvent;
37 import android.view.View;
38 import android.view.ViewGroup;
39 
40 import androidx.test.InstrumentationRegistry;
41 import androidx.test.rule.ActivityTestRule;
42 import androidx.test.runner.AndroidJUnit4;
43 
44 import org.junit.After;
45 import org.junit.Before;
46 import org.junit.Rule;
47 import org.junit.Test;
48 import org.junit.runner.RunWith;
49 
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.concurrent.CountDownLatch;
53 import java.util.concurrent.TimeUnit;
54 import java.util.stream.IntStream;
55 
56 @RunWith(AndroidJUnit4.class)
57 public class DragDropTest {
58     static final String TAG = "DragDropTest";
59 
60     final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
61     final UiAutomation mAutomation = mInstrumentation.getUiAutomation();
62 
63     @Rule
64     public ActivityTestRule<DragDropActivity> mActivityRule =
65             new ActivityTestRule<>(DragDropActivity.class);
66 
67     private DragDropActivity mActivity;
68 
69     private CountDownLatch mStartReceived;
70     private CountDownLatch mEndReceived;
71 
72     private AssertionError mMainThreadAssertionError;
73 
74     /**
75      * Check whether two objects have the same binary data when dumped into Parcels
76      * @return True if the objects are equal
77      */
compareParcelables(Parcelable obj1, Parcelable obj2)78     private static boolean compareParcelables(Parcelable obj1, Parcelable obj2) {
79         if (obj1 == null && obj2 == null) {
80             return true;
81         }
82         if (obj1 == null || obj2 == null) {
83             return false;
84         }
85         Parcel p1 = Parcel.obtain();
86         obj1.writeToParcel(p1, 0);
87         Parcel p2 = Parcel.obtain();
88         obj2.writeToParcel(p2, 0);
89         boolean result = Arrays.equals(p1.marshall(), p2.marshall());
90         p1.recycle();
91         p2.recycle();
92         return result;
93     }
94 
95     private static final ClipDescription sClipDescription =
96             new ClipDescription("TestLabel", new String[]{"text/plain"});
97     private static final ClipData sClipData =
98             new ClipData(sClipDescription, new ClipData.Item("TestText"));
99     private static final Object sLocalState = new Object(); // just check if null or not
100 
101     class LogEntry {
102         public View view;
103 
104         // Public DragEvent fields
105         public int action; // DragEvent.getAction()
106         public float x; // DragEvent.getX()
107         public float y; // DragEvent.getY()
108         public ClipData clipData; // DragEvent.getClipData()
109         public ClipDescription clipDescription; // DragEvent.getClipDescription()
110         public Object localState; // DragEvent.getLocalState()
111         public boolean result; // DragEvent.getResult()
112 
LogEntry(View v, int action, float x, float y, ClipData clipData, ClipDescription clipDescription, Object localState, boolean result)113         LogEntry(View v, int action, float x, float y, ClipData clipData,
114                 ClipDescription clipDescription, Object localState, boolean result) {
115             this.view = v;
116             this.action = action;
117             this.x = x;
118             this.y = y;
119             this.clipData = clipData;
120             this.clipDescription = clipDescription;
121             this.localState = localState;
122             this.result = result;
123         }
124 
125         @Override
equals(Object obj)126         public boolean equals(Object obj) {
127             if (this == obj) {
128                 return true;
129             }
130             if (!(obj instanceof LogEntry)) {
131                 return false;
132             }
133             final LogEntry other = (LogEntry) obj;
134             return view == other.view && action == other.action
135                     && x == other.x && y == other.y
136                     && compareParcelables(clipData, other.clipData)
137                     && compareParcelables(clipDescription, other.clipDescription)
138                     && localState == other.localState
139                     && result == other.result;
140         }
141 
142         @Override
toString()143         public String toString() {
144             StringBuilder sb = new StringBuilder();
145             sb.append("DragEvent {action=").append(action).append(" x=").append(x).append(" y=")
146                     .append(y).append(" result=").append(result).append("}")
147                     .append(" @ ").append(view);
148             return sb.toString();
149         }
150     }
151 
152     // Actual and expected sequences of events.
153     // While the test is running, logs should be accessed only from the main thread.
154     final private ArrayList<LogEntry> mActual = new ArrayList<LogEntry> ();
155     final private ArrayList<LogEntry> mExpected = new ArrayList<LogEntry> ();
156 
obtainClipData(int action)157     private static ClipData obtainClipData(int action) {
158         if (action == DragEvent.ACTION_DROP) {
159             return sClipData;
160         }
161         return null;
162     }
163 
obtainClipDescription(int action)164     private static ClipDescription obtainClipDescription(int action) {
165         if (action == DragEvent.ACTION_DRAG_ENDED) {
166             return null;
167         }
168         return sClipDescription;
169     }
170 
logEvent(View v, DragEvent ev)171     private void logEvent(View v, DragEvent ev) {
172         if (ev.getAction() == DragEvent.ACTION_DRAG_STARTED) {
173             mStartReceived.countDown();
174         }
175         if (ev.getAction() == DragEvent.ACTION_DRAG_ENDED) {
176             mEndReceived.countDown();
177         }
178         mActual.add(new LogEntry(v, ev.getAction(), ev.getX(), ev.getY(), ev.getClipData(),
179                 ev.getClipDescription(), ev.getLocalState(), ev.getResult()));
180     }
181 
182     // Add expected event for a view, with zero coordinates.
expectEvent5(int action, int viewId)183     private void expectEvent5(int action, int viewId) {
184         View v = mActivity.findViewById(viewId);
185         mExpected.add(new LogEntry(v, action, 0, 0, obtainClipData(action),
186                 obtainClipDescription(action), sLocalState, false));
187     }
188 
189     // Add expected event for a view.
expectEndEvent(int viewId, float x, float y, boolean result)190     private void expectEndEvent(int viewId, float x, float y, boolean result) {
191         View v = mActivity.findViewById(viewId);
192         int action = DragEvent.ACTION_DRAG_ENDED;
193         mExpected.add(new LogEntry(v, action, x, y, obtainClipData(action),
194                 obtainClipDescription(action), sLocalState, result));
195     }
196 
197     // Add expected successful-end event for a view.
expectEndEventSuccess(int viewId)198     private void expectEndEventSuccess(int viewId) {
199         expectEndEvent(viewId, 0, 0, true);
200     }
201 
202     // Add expected failed-end event for a view, with the release coordinates shifted by 6 relative
203     // to the left-upper corner of a view with id releaseViewId.
expectEndEventFailure6(int viewId, int releaseViewId)204     private void expectEndEventFailure6(int viewId, int releaseViewId) {
205         View v = mActivity.findViewById(viewId);
206         View release = mActivity.findViewById(releaseViewId);
207         int [] releaseLoc = new int[2];
208         release.getLocationOnScreen(releaseLoc);
209         int action = DragEvent.ACTION_DRAG_ENDED;
210         mExpected.add(new LogEntry(v, action,
211                 releaseLoc[0] + 6, releaseLoc[1] + 6, obtainClipData(action),
212                 obtainClipDescription(action), sLocalState, false));
213     }
214 
215     // Add expected event for a view, with coordinates over view locationViewId, with the specified
216     // offset from the location view's upper-left corner.
expectEventWithOffset(int action, int viewId, int locationViewId, int offset)217     private void expectEventWithOffset(int action, int viewId, int locationViewId, int offset) {
218         View v = mActivity.findViewById(viewId);
219         View locationView = mActivity.findViewById(locationViewId);
220         int [] viewLocation = new int[2];
221         v.getLocationOnScreen(viewLocation);
222         int [] locationViewLocation = new int[2];
223         locationView.getLocationOnScreen(locationViewLocation);
224         mExpected.add(new LogEntry(v, action,
225                 locationViewLocation[0] - viewLocation[0] + offset,
226                 locationViewLocation[1] - viewLocation[1] + offset, obtainClipData(action),
227                 obtainClipDescription(action), sLocalState, false));
228     }
229 
expectEvent5(int action, int viewId, int locationViewId)230     private void expectEvent5(int action, int viewId, int locationViewId) {
231         expectEventWithOffset(action, viewId, locationViewId, 5);
232     }
233 
234     // See comment for injectMouse6 on why we need both *5 and *6 methods.
expectEvent6(int action, int viewId, int locationViewId)235     private void expectEvent6(int action, int viewId, int locationViewId) {
236         expectEventWithOffset(action, viewId, locationViewId, 6);
237     }
238 
239     // Inject mouse event over a given view, with specified offset from its left-upper corner.
injectMouseWithOffset(int viewId, int action, int offset)240     private void injectMouseWithOffset(int viewId, int action, int offset) {
241         runOnMain(() -> {
242             View v = mActivity.findViewById(viewId);
243             int [] destLoc = new int [2];
244             v.getLocationOnScreen(destLoc);
245             long downTime = SystemClock.uptimeMillis();
246             MotionEvent event = MotionEvent.obtain(downTime, downTime, action,
247                     destLoc[0] + offset, destLoc[1] + offset, 1);
248             event.setSource(InputDevice.SOURCE_MOUSE);
249             mAutomation.injectInputEvent(event, false);
250         });
251 
252         // Wait till the mouse event generates drag events. Also, some waiting needed because the
253         // system seems to collapse too frequent mouse events.
254         try {
255             Thread.sleep(100);
256         } catch (Exception e) {
257             fail("Exception while wait: " + e);
258         }
259     }
260 
261     // Inject mouse event over a given view, with offset 5 from its left-upper corner.
injectMouse5(int viewId, int action)262     private void injectMouse5(int viewId, int action) {
263         injectMouseWithOffset(viewId, action, 5);
264     }
265 
266     // Inject mouse event over a given view, with offset 6 from its left-upper corner.
267     // We need both injectMouse5 and injectMouse6 if we want to inject 2 events in a row in the same
268     // view, and want them to produce distinct drag events or simply drag events with different
269     // coordinates.
injectMouse6(int viewId, int action)270     private void injectMouse6(int viewId, int action) {
271         injectMouseWithOffset(viewId, action, 6);
272     }
273 
logToString(ArrayList<LogEntry> log)274     private String logToString(ArrayList<LogEntry> log) {
275         StringBuilder sb = new StringBuilder();
276         for (int i = 0; i < log.size(); ++i) {
277             LogEntry e = log.get(i);
278             sb.append("#").append(i + 1).append(": ").append(e).append('\n');
279         }
280         return sb.toString();
281     }
282 
failWithLogs(String message)283     private void failWithLogs(String message) {
284         fail(message + ":\nExpected event sequence:\n" + logToString(mExpected) +
285                 "\nActual event sequence:\n" + logToString(mActual));
286     }
287 
verifyEventLog()288     private void verifyEventLog() {
289         try {
290             assertTrue("Timeout while waiting for END event",
291                     mEndReceived.await(1, TimeUnit.SECONDS));
292         } catch (InterruptedException e) {
293             fail("Got InterruptedException while waiting for END event");
294         }
295 
296         // Verify the log.
297         runOnMain(() -> {
298             if (mExpected.size() != mActual.size()) {
299                 failWithLogs("Actual log has different size than expected");
300             }
301 
302             for (int i = 0; i < mActual.size(); ++i) {
303                 if (!mActual.get(i).equals(mExpected.get(i))) {
304                     failWithLogs("Actual event #" + (i + 1) + " is different from expected");
305                 }
306             }
307         });
308     }
309 
init()310     private boolean init() {
311         // Only run for non-watch devices
312         if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) {
313             return false;
314         }
315         return true;
316     }
317 
318     @Before
setUp()319     public void setUp() {
320         mActivity = mActivityRule.getActivity();
321         mStartReceived = new CountDownLatch(1);
322         mEndReceived = new CountDownLatch(1);
323 
324         // Wait for idle
325         mInstrumentation.waitForIdleSync();
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         if (!init()) {
391             return;
392         }
393 
394         runOnMain(() -> {
395             // Tell all views in layout to return false to all events, and log them.
396             setRejectingHandlersOnTree(mActivity.findViewById(R.id.drag_drop_activity_main));
397 
398             // Override handlers for the inner view and its parent to return true.
399             mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
400                 logEvent(v, ev);
401                 return true;
402             });
403             mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> {
404                 logEvent(v, ev);
405                 return true;
406             });
407         });
408 
409         startDrag();
410 
411         // Move mouse to the outmost view. This shouldn't generate any events since it returned
412         // false to STARTED.
413         injectMouse5(R.id.container, MotionEvent.ACTION_MOVE);
414         // Release mouse over the inner view. This produces DROP there.
415         injectMouse5(R.id.inner, MotionEvent.ACTION_UP);
416 
417         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable);
418         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable);
419         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable);
420         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.draggable, R.id.draggable);
421         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.drag_drop_activity_main, R.id.draggable);
422 
423         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner);
424         expectEvent5(DragEvent.ACTION_DROP, R.id.inner, R.id.inner);
425 
426         expectEndEventSuccess(R.id.inner);
427         expectEndEventSuccess(R.id.subcontainer);
428 
429         verifyEventLog();
430     }
431 
432     /**
433      * Tests events over a non-accepting view with an accepting child get delivered to that view's
434      * parent.
435      */
436     @Test
testBlackHole()437     public void testBlackHole() throws Exception {
438         if (!init()) {
439             return;
440         }
441 
442         runOnMain(() -> {
443             // Accepting child.
444             mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
445                 logEvent(v, ev);
446                 return true;
447             });
448             // Non-accepting parent of that child.
449             mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> {
450                 logEvent(v, ev);
451                 return false;
452             });
453             // Accepting parent of the previous view.
454             mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> {
455                 logEvent(v, ev);
456                 return true;
457             });
458         });
459 
460         startDrag();
461 
462         // Move mouse to the non-accepting view.
463         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
464         // Release mouse over the non-accepting view, with different coordinates.
465         injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP);
466 
467         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable);
468         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable);
469         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable);
470 
471         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container);
472         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer);
473         expectEvent6(DragEvent.ACTION_DROP, R.id.container, R.id.subcontainer);
474 
475         expectEndEventSuccess(R.id.inner);
476         expectEndEventSuccess(R.id.container);
477 
478         verifyEventLog();
479     }
480 
481     /**
482      * Tests generation of ENTER/EXIT events.
483      */
484     @Test
testEnterExit()485     public void testEnterExit() throws Exception {
486         if (!init()) {
487             return;
488         }
489 
490         runOnMain(() -> {
491             // The setup is same as for testBlackHole.
492 
493             // Accepting child.
494             mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
495                 logEvent(v, ev);
496                 return true;
497             });
498             // Non-accepting parent of that child.
499             mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> {
500                 logEvent(v, ev);
501                 return false;
502             });
503             // Accepting parent of the previous view.
504             mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> {
505                 logEvent(v, ev);
506                 return true;
507             });
508 
509         });
510 
511         startDrag();
512 
513         // Move mouse to the non-accepting view, then to the inner one, then back to the
514         // non-accepting view, then release over the inner.
515         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
516         injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE);
517         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
518         injectMouse5(R.id.inner, MotionEvent.ACTION_UP);
519 
520         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable);
521         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable);
522         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable);
523 
524         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container);
525         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer);
526         expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container);
527 
528         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner);
529         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.inner, R.id.inner);
530         expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.inner);
531 
532         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container);
533         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer);
534         expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container);
535 
536         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner);
537         expectEvent5(DragEvent.ACTION_DROP, R.id.inner, R.id.inner);
538 
539         expectEndEventSuccess(R.id.inner);
540         expectEndEventSuccess(R.id.container);
541 
542         verifyEventLog();
543     }
544     /**
545      * Tests events over a non-accepting view that has no accepting ancestors.
546      */
547     @Test
testOverNowhere()548     public void testOverNowhere() throws Exception {
549         if (!init()) {
550             return;
551         }
552 
553         runOnMain(() -> {
554             // Accepting child.
555             mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
556                 logEvent(v, ev);
557                 return true;
558             });
559             // Non-accepting parent of that child.
560             mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> {
561                 logEvent(v, ev);
562                 return false;
563             });
564         });
565 
566         startDrag();
567 
568         // Move mouse to the non-accepting view, then to accepting view, and back, and drop there.
569         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
570         injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE);
571         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
572         injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP);
573 
574         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable);
575         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable);
576 
577         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner);
578         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.inner, R.id.inner);
579         expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.inner);
580 
581         expectEndEventFailure6(R.id.inner, R.id.subcontainer);
582 
583         verifyEventLog();
584     }
585 
586     /**
587      * Tests that events are properly delivered to a view that is in the middle of the accepting
588      * hierarchy.
589      */
590     @Test
testAcceptingGroupInTheMiddle()591     public void testAcceptingGroupInTheMiddle() throws Exception {
592         if (!init()) {
593             return;
594         }
595 
596         runOnMain(() -> {
597             // Set accepting handlers to the inner view and its 2 ancestors.
598             mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
599                 logEvent(v, ev);
600                 return true;
601             });
602             mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> {
603                 logEvent(v, ev);
604                 return true;
605             });
606             mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> {
607                 logEvent(v, ev);
608                 return true;
609             });
610         });
611 
612         startDrag();
613 
614         // Move mouse to the outmost container, then move to the subcontainer and drop there.
615         injectMouse5(R.id.container, MotionEvent.ACTION_MOVE);
616         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
617         injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP);
618 
619         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable);
620         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable);
621         expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable);
622 
623         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container);
624         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.container);
625         expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container);
626 
627         expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.subcontainer);
628         expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.subcontainer, R.id.subcontainer);
629         expectEvent6(DragEvent.ACTION_DROP, R.id.subcontainer, R.id.subcontainer);
630 
631         expectEndEventSuccess(R.id.inner);
632         expectEndEventSuccess(R.id.subcontainer);
633         expectEndEventSuccess(R.id.container);
634 
635         verifyEventLog();
636     }
637 
drawableStateContains(int resourceId, int attr)638     private boolean drawableStateContains(int resourceId, int attr) {
639         return IntStream.of(mActivity.findViewById(resourceId).getDrawableState())
640                 .anyMatch(x -> x == attr);
641     }
642 
643     /**
644      * Tests that state_drag_hovered and state_drag_can_accept are set correctly.
645      */
646     @Test
testDrawableState()647     public void testDrawableState() throws Exception {
648         if (!init()) {
649             return;
650         }
651 
652         runOnMain(() -> {
653             // Set accepting handler for the inner view.
654             mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
655                 logEvent(v, ev);
656                 return true;
657             });
658             assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_can_accept));
659         });
660 
661         startDrag();
662 
663         runOnMain(() -> {
664             assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
665             assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_can_accept));
666         });
667 
668         // Move mouse into the view.
669         injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE);
670         runOnMain(() -> {
671             assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
672         });
673 
674         // Move out.
675         injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
676         runOnMain(() -> {
677             assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
678         });
679 
680         // Move in.
681         injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE);
682         runOnMain(() -> {
683             assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
684         });
685 
686         // Release there.
687         injectMouse5(R.id.inner, MotionEvent.ACTION_UP);
688         runOnMain(() -> {
689             assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
690         });
691     }
692 }