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