Home | History | Annotate | Download | only in wm
      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      */
     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 
    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
    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
    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 
    157     private static ClipData obtainClipData(int action) {
    158         if (action == DragEvent.ACTION_DROP) {
    159             return sClipData;
    160         }
    161         return null;
    162     }
    163 
    164     private static ClipDescription obtainClipDescription(int action) {
    165         if (action == DragEvent.ACTION_DRAG_ENDED) {
    166             return null;
    167         }
    168         return sClipDescription;
    169     }
    170 
    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.
    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.
    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.
    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.
    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.
    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 
    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.
    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.
    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.
    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.
    270     private void injectMouse6(int viewId, int action) {
    271         injectMouseWithOffset(viewId, action, 6);
    272     }
    273 
    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 
    283     private void failWithLogs(String message) {
    284         fail(message + ":\nExpected event sequence:\n" + logToString(mExpected) +
    285                 "\nActual event sequence:\n" + logToString(mActual));
    286     }
    287 
    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 
    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
    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
    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.
    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 
    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 
    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
    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
    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
    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
    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
    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 
    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
    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 }