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