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