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