| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.view.cts; |
| |
| import com.android.internal.util.ArrayUtils; |
| |
| import android.app.Instrumentation; |
| import android.app.UiAutomation; |
| import android.content.pm.PackageManager; |
| import android.os.SystemClock; |
| import android.support.test.InstrumentationRegistry; |
| import android.support.test.rule.ActivityTestRule; |
| import android.support.test.runner.AndroidJUnit4; |
| import android.view.DragEvent; |
| import android.view.InputDevice; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| |
| import java.lang.InterruptedException; |
| import java.lang.StringBuilder; |
| import java.lang.Thread; |
| import java.util.ArrayList; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.Objects; |
| |
| import static junit.framework.TestCase.*; |
| |
| @RunWith(AndroidJUnit4.class) |
| public class DragDropTest { |
| static final String TAG = "DragDropTest"; |
| |
| final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); |
| final UiAutomation mAutomation = mInstrumentation.getUiAutomation(); |
| |
| @Rule |
| public ActivityTestRule<DragDropActivity> mActivityRule = |
| new ActivityTestRule<>(DragDropActivity.class); |
| |
| private DragDropActivity mActivity; |
| |
| private CountDownLatch mEndReceived; |
| |
| static boolean equal(DragEvent ev1, DragEvent ev2) { |
| return ev1.getAction() == ev2.getAction() && |
| ev1.getX() == ev2.getX() && |
| ev1.getY() == ev2.getY() && |
| Objects.equals(ev1.getClipData(), ev2.getClipData()) && |
| Objects.equals(ev1.getClipDescription(), ev2.getClipDescription()) && |
| Objects.equals(ev1.getDragAndDropPermissions(), ev2.getDragAndDropPermissions()) && |
| Objects.equals(ev1.getLocalState(), ev2.getLocalState()) && |
| ev1.getResult() == ev2.getResult(); |
| } |
| |
| class LogEntry { |
| public View v; |
| public DragEvent ev; |
| |
| public LogEntry(View v, DragEvent ev) { |
| this.v = v; |
| this.ev = DragEvent.obtain(ev); |
| } |
| |
| public boolean eq(LogEntry other) { |
| return v == other.v && equal(ev, other.ev); |
| } |
| } |
| |
| // Actual and expected sequences of events. |
| // While the test is running, logs should be accessed only from the main thread. |
| final private ArrayList<LogEntry> mActual = new ArrayList<LogEntry> (); |
| final private ArrayList<LogEntry> mExpected = new ArrayList<LogEntry> (); |
| |
| static private DragEvent obtainDragEvent(int action, int x, int y, boolean result) { |
| return DragEvent.obtain(action, x, y, null, null, null, null, result); |
| } |
| |
| private void logEvent(View v, DragEvent ev) { |
| if (ev.getAction() == DragEvent.ACTION_DRAG_ENDED) { |
| mEndReceived.countDown(); |
| } |
| mActual.add(new LogEntry(v, ev)); |
| } |
| |
| // Add expected event for a view, with zero coordinates. |
| private void expectEvent5(int action, int viewId) { |
| View v = mActivity.findViewById(viewId); |
| mExpected.add(new LogEntry(v, obtainDragEvent(action, 0, 0, false))); |
| } |
| |
| // Add expected event for a view. |
| private void expectEndEvent(int viewId, int x, int y, boolean result) { |
| View v = mActivity.findViewById(viewId); |
| mExpected.add(new LogEntry(v, obtainDragEvent(DragEvent.ACTION_DRAG_ENDED, x, y, result))); |
| } |
| |
| // Add expected successful-end event for a view. |
| private void expectEndEventSuccess(int viewId) { |
| expectEndEvent(viewId, 0, 0, true); |
| } |
| |
| // Add expected failed-end event for a view, with the release coordinates shifted by 6 relative |
| // to the left-upper corner of a view with id releaseViewId. |
| private void expectEndEventFailure6(int viewId, int releaseViewId) { |
| View v = mActivity.findViewById(viewId); |
| View release = mActivity.findViewById(releaseViewId); |
| int [] releaseLoc = release.getLocationOnScreen(); |
| mExpected.add(new LogEntry(v, obtainDragEvent(DragEvent.ACTION_DRAG_ENDED, |
| releaseLoc[0] + 6, releaseLoc[1] + 6, false))); |
| } |
| |
| // Add expected event for a view, with coordinates over view locationViewId, with the specified |
| // offset from the location view's upper-left corner. |
| private void expectEventWithOffset(int action, int viewId, int locationViewId, int offset) { |
| View v = mActivity.findViewById(viewId); |
| View locationView = mActivity.findViewById(locationViewId); |
| int [] viewLocation = v.getLocationOnScreen(); |
| int [] locationViewLocation = locationView.getLocationOnScreen(); |
| mExpected.add(new LogEntry(v, obtainDragEvent(action, |
| locationViewLocation[0] - viewLocation[0] + offset, |
| locationViewLocation[1] - viewLocation[1] + offset, false))); |
| } |
| |
| private void expectEvent5(int action, int viewId, int locationViewId) { |
| expectEventWithOffset(action, viewId, locationViewId, 5); |
| } |
| |
| // See comment for injectMouse6 on why we need both *5 and *6 methods. |
| private void expectEvent6(int action, int viewId, int locationViewId) { |
| expectEventWithOffset(action, viewId, locationViewId, 6); |
| } |
| |
| // Inject mouse event over a given view, with specified offset from its left-upper corner. |
| private void injectMouseWithOffset(int viewId, int action, int offset) { |
| runOnMain(() -> { |
| View v = mActivity.findViewById(viewId); |
| int [] destLoc = v.getLocationOnScreen(); |
| long downTime = SystemClock.uptimeMillis(); |
| MotionEvent event = MotionEvent.obtain(downTime, downTime, action, |
| destLoc[0] + offset, destLoc[1] + offset, 1); |
| event.setSource(InputDevice.SOURCE_TOUCHSCREEN); |
| mAutomation.injectInputEvent(event, false); |
| }); |
| |
| // Wait till the mouse event generates drag events. Also, some waiting needed because the |
| // system seems to collapse too frequent mouse events. |
| try { |
| Thread.sleep(100); |
| } catch (Exception e) { |
| fail("Exception while wait: " + e); |
| } |
| } |
| |
| // Inject mouse event over a given view, with offset 5 from its left-upper corner. |
| private void injectMouse5(int viewId, int action) { |
| injectMouseWithOffset(viewId, action, 5); |
| } |
| |
| // Inject mouse event over a given view, with offset 6 from its left-upper corner. |
| // We need both injectMouse5 and injectMouse6 if we want to inject 2 events in a row in the same |
| // view, and want them to produce distinct drag events or simply drag events with different |
| // coordinates. |
| private void injectMouse6(int viewId, int action) { |
| injectMouseWithOffset(viewId, action, 6); |
| } |
| |
| private String logToString(ArrayList<LogEntry> log) { |
| StringBuilder sb = new StringBuilder(); |
| for (int i = 0; i < log.size(); ++i) { |
| LogEntry e = log.get(i); |
| sb.append("#").append(i + 1).append(": ").append(e.ev).append(" @ "). |
| append(e.v.toString()).append('\n'); |
| } |
| return sb.toString(); |
| } |
| |
| private void failWithLogs(String message) { |
| fail(message + ":\nExpected event sequence:\n" + logToString(mExpected) + |
| "\nActual event sequence:\n" + logToString(mActual)); |
| } |
| |
| private void verifyEventLog() { |
| try { |
| assertTrue("Timeout while waiting for END event", |
| mEndReceived.await(1, TimeUnit.SECONDS)); |
| } catch (InterruptedException e) { |
| fail("Got InterruptedException while waiting for END event"); |
| } |
| |
| // Verify the log. |
| runOnMain(() -> { |
| if (mExpected.size() != mActual.size()) { |
| failWithLogs("Actual log has different size than expected"); |
| } |
| |
| for (int i = 0; i < mActual.size(); ++i) { |
| if (!mActual.get(i).eq(mExpected.get(i))) { |
| failWithLogs("Actual event #" + (i + 1) + " is different from expected"); |
| } |
| } |
| }); |
| } |
| |
| private boolean init() { |
| // Only run for non-watch devices |
| if (mActivity.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { |
| return false; |
| } |
| return true; |
| } |
| |
| @Before |
| public void setUp() { |
| mActivity = mActivityRule.getActivity(); |
| mEndReceived = new CountDownLatch(1); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| mActual.clear(); |
| mExpected.clear(); |
| } |
| |
| // Sets handlers on all views in a tree, which log the event and return false. |
| private void setRejectingHandlersOnTree(View v) { |
| v.setOnDragListener((_v, ev) -> { |
| logEvent(_v, ev); |
| return false; |
| }); |
| |
| if (v instanceof ViewGroup) { |
| ViewGroup group = (ViewGroup) v; |
| for (int i = 0; i < group.getChildCount(); ++i) { |
| setRejectingHandlersOnTree(group.getChildAt(i)); |
| } |
| } |
| } |
| |
| private void runOnMain(Runnable runner) { |
| mInstrumentation.runOnMainSync(runner); |
| } |
| |
| private void startDrag() { |
| // Mouse down. Required for the drag to start. |
| injectMouse5(R.id.draggable, MotionEvent.ACTION_DOWN); |
| |
| runOnMain(() -> { |
| // Start drag. |
| View v = mActivity.findViewById(R.id.draggable); |
| assertTrue("Couldn't start drag", |
| v.startDragAndDrop(null, new View.DragShadowBuilder(v), null, 0)); |
| }); |
| } |
| |
| /** |
| * Tests that no drag-drop events are sent to views that aren't supposed to receive them. |
| */ |
| @Test |
| public void testNoExtraEvents() throws Exception { |
| if (!init()) { |
| return; |
| } |
| |
| runOnMain(() -> { |
| // Tell all views in layout to return false to all events, and log them. |
| setRejectingHandlersOnTree(mActivity.findViewById(R.id.drag_drop_activity_main)); |
| |
| // Override handlers for the inner view and its parent to return true. |
| mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| }); |
| |
| startDrag(); |
| |
| // Move mouse to the outmost view. This shouldn't generate any events since it returned |
| // false to STARTED. |
| injectMouse5(R.id.container, MotionEvent.ACTION_MOVE); |
| // Release mouse over the inner view. This produces DROP there. |
| injectMouse5(R.id.inner, MotionEvent.ACTION_UP); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.draggable, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.drag_drop_activity_main, R.id.draggable); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); |
| expectEvent5(DragEvent.ACTION_DROP, R.id.inner, R.id.inner); |
| |
| expectEndEventSuccess(R.id.inner); |
| expectEndEventSuccess(R.id.subcontainer); |
| |
| verifyEventLog(); |
| } |
| |
| /** |
| * Tests events over a non-accepting view with an accepting child get delivered to that view's |
| * parent. |
| */ |
| @Test |
| public void testBlackHole() throws Exception { |
| if (!init()) { |
| return; |
| } |
| |
| runOnMain(() -> { |
| // Accepting child. |
| mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| // Non-accepting parent of that child. |
| mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return false; |
| }); |
| // Accepting parent of the previous view. |
| mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| }); |
| |
| startDrag(); |
| |
| // Move mouse to the non-accepting view. |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| // Release mouse over the non-accepting view, with different coordinates. |
| injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer); |
| expectEvent6(DragEvent.ACTION_DROP, R.id.container, R.id.subcontainer); |
| |
| expectEndEventSuccess(R.id.inner); |
| expectEndEventSuccess(R.id.container); |
| |
| verifyEventLog(); |
| } |
| |
| /** |
| * Tests generation of ENTER/EXIT events. |
| */ |
| @Test |
| public void testEnterExit() throws Exception { |
| if (!init()) { |
| return; |
| } |
| |
| runOnMain(() -> { |
| // The setup is same as for testBlackHole. |
| |
| // Accepting child. |
| mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| // Non-accepting parent of that child. |
| mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return false; |
| }); |
| // Accepting parent of the previous view. |
| mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| |
| }); |
| |
| startDrag(); |
| |
| // Move mouse to the non-accepting view, then to the inner one, then back to the |
| // non-accepting view, then release over the inner. |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| injectMouse5(R.id.inner, MotionEvent.ACTION_UP); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer); |
| expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.inner, R.id.inner); |
| expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.inner); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.subcontainer); |
| expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); |
| expectEvent5(DragEvent.ACTION_DROP, R.id.inner, R.id.inner); |
| |
| expectEndEventSuccess(R.id.inner); |
| expectEndEventSuccess(R.id.container); |
| |
| verifyEventLog(); |
| } |
| /** |
| * Tests events over a non-accepting view that has no accepting ancestors. |
| */ |
| @Test |
| public void testOverNowhere() throws Exception { |
| if (!init()) { |
| return; |
| } |
| |
| runOnMain(() -> { |
| // Accepting child. |
| mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| // Non-accepting parent of that child. |
| mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return false; |
| }); |
| }); |
| |
| startDrag(); |
| |
| // Move mouse to the non-accepting view, then to accepting view, and back, and drop there. |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.inner); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.inner, R.id.inner); |
| expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.inner); |
| |
| expectEndEventFailure6(R.id.inner, R.id.subcontainer); |
| |
| verifyEventLog(); |
| } |
| |
| /** |
| * Tests that events are properly delivered to a view that is in the middle of the accepting |
| * hierarchy. |
| */ |
| @Test |
| public void testAcceptingGroupInTheMiddle() throws Exception { |
| if (!init()) { |
| return; |
| } |
| |
| runOnMain(() -> { |
| // Set accepting handlers to the inner view and its 2 ancestors. |
| mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| mActivity.findViewById(R.id.subcontainer).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| mActivity.findViewById(R.id.container).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| }); |
| |
| startDrag(); |
| |
| // Move mouse to the outmost container, then move to the subcontainer and drop there. |
| injectMouse5(R.id.container, MotionEvent.ACTION_MOVE); |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| injectMouse6(R.id.subcontainer, MotionEvent.ACTION_UP); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.inner, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.subcontainer, R.id.draggable); |
| expectEvent5(DragEvent.ACTION_DRAG_STARTED, R.id.container, R.id.draggable); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.container); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.container, R.id.container); |
| expectEvent5(DragEvent.ACTION_DRAG_EXITED, R.id.container); |
| |
| expectEvent5(DragEvent.ACTION_DRAG_ENTERED, R.id.subcontainer); |
| expectEvent5(DragEvent.ACTION_DRAG_LOCATION, R.id.subcontainer, R.id.subcontainer); |
| expectEvent6(DragEvent.ACTION_DROP, R.id.subcontainer, R.id.subcontainer); |
| |
| expectEndEventSuccess(R.id.inner); |
| expectEndEventSuccess(R.id.subcontainer); |
| expectEndEventSuccess(R.id.container); |
| |
| verifyEventLog(); |
| } |
| |
| /** |
| * Tests that state_drag_hovered and state_drag_can_accept are set correctly. |
| */ |
| @Test |
| public void testDrawableState() throws Exception { |
| if (!init()) { |
| return; |
| } |
| |
| runOnMain(() -> { |
| // Set accepting handler for the inner view. |
| mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> { |
| logEvent(v, ev); |
| return true; |
| }); |
| assertFalse(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_can_accept)); |
| }); |
| |
| startDrag(); |
| |
| runOnMain(() -> { |
| assertFalse(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_hovered)); |
| assertTrue(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_can_accept)); |
| }); |
| |
| // Move mouse into the view. |
| injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); |
| runOnMain(() -> { |
| assertTrue(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_hovered)); |
| }); |
| |
| // Move out. |
| injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE); |
| runOnMain(() -> { |
| assertFalse(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_hovered)); |
| }); |
| |
| // Move in. |
| injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE); |
| runOnMain(() -> { |
| assertTrue(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_hovered)); |
| }); |
| |
| // Release there. |
| injectMouse5(R.id.inner, MotionEvent.ACTION_UP); |
| runOnMain(() -> { |
| assertFalse(ArrayUtils.contains( |
| mActivity.findViewById(R.id.inner).getDrawableState(), |
| android.R.attr.state_drag_hovered)); |
| }); |
| } |
| } |