/*
 * 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.server.wm;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;

import android.app.Instrumentation;
import android.app.UiAutomation;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.platform.test.annotations.Presubmit;
import android.server.wm.cts.R;
import android.util.MutableBoolean;
import android.view.DragEvent;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.FlakyTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.IntStream;

@Presubmit
@RunWith(AndroidJUnit4.class)
public class DragDropTest extends WindowManagerTestBase {
    static final String TAG = "DragDropTest";

    final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
    final UiAutomation mAutomation = mInstrumentation.getUiAutomation();

    private DragDropActivity mActivity;

    private CountDownLatch mStartReceived;
    private CountDownLatch mEndReceived;

    private AssertionError mMainThreadAssertionError;

    /**
     * Check whether two objects have the same binary data when dumped into Parcels
     * @return True if the objects are equal
     */
    private static boolean compareParcelables(Parcelable obj1, Parcelable obj2) {
        if (obj1 == null && obj2 == null) {
            return true;
        }
        if (obj1 == null || obj2 == null) {
            return false;
        }
        Parcel p1 = Parcel.obtain();
        obj1.writeToParcel(p1, 0);
        Parcel p2 = Parcel.obtain();
        obj2.writeToParcel(p2, 0);
        boolean result = Arrays.equals(p1.marshall(), p2.marshall());
        p1.recycle();
        p2.recycle();
        return result;
    }

    private static final ClipDescription sClipDescription =
            new ClipDescription("TestLabel", new String[]{"text/plain"});
    private static final ClipData sClipData =
            new ClipData(sClipDescription, new ClipData.Item("TestText"));
    private static final Object sLocalState = new Object(); // just check if null or not

    class LogEntry {
        public View view;

        // Public DragEvent fields
        public int action; // DragEvent.getAction()
        public float x; // DragEvent.getX()
        public float y; // DragEvent.getY()
        public ClipData clipData; // DragEvent.getClipData()
        public ClipDescription clipDescription; // DragEvent.getClipDescription()
        public Object localState; // DragEvent.getLocalState()
        public boolean result; // DragEvent.getResult()

        LogEntry(View v, int action, float x, float y, ClipData clipData,
                ClipDescription clipDescription, Object localState, boolean result) {
            this.view = v;
            this.action = action;
            this.x = x;
            this.y = y;
            this.clipData = clipData;
            this.clipDescription = clipDescription;
            this.localState = localState;
            this.result = result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof LogEntry)) {
                return false;
            }
            final LogEntry other = (LogEntry) obj;
            return view == other.view && action == other.action
                    && x == other.x && y == other.y
                    && compareParcelables(clipData, other.clipData)
                    && compareParcelables(clipDescription, other.clipDescription)
                    && localState == other.localState
                    && result == other.result;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("DragEvent {action=").append(action).append(" x=").append(x).append(" y=")
                    .append(y).append(" result=").append(result).append("}")
                    .append(" @ ").append(view);
            return sb.toString();
        }
    }

    // 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> ();

    private static ClipData obtainClipData(int action) {
        if (action == DragEvent.ACTION_DROP) {
            return sClipData;
        }
        return null;
    }

    private static ClipDescription obtainClipDescription(int action) {
        if (action == DragEvent.ACTION_DRAG_ENDED) {
            return null;
        }
        return sClipDescription;
    }

    private void logEvent(View v, DragEvent ev) {
        if (ev.getAction() == DragEvent.ACTION_DRAG_STARTED) {
            mStartReceived.countDown();
        }
        if (ev.getAction() == DragEvent.ACTION_DRAG_ENDED) {
            mEndReceived.countDown();
        }
        mActual.add(new LogEntry(v, ev.getAction(), ev.getX(), ev.getY(), ev.getClipData(),
                ev.getClipDescription(), ev.getLocalState(), ev.getResult()));
    }

    // 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, action, 0, 0, obtainClipData(action),
                obtainClipDescription(action), sLocalState, false));
    }

    // Add expected event for a view.
    private void expectEndEvent(int viewId, float x, float y, boolean result) {
        View v = mActivity.findViewById(viewId);
        int action = DragEvent.ACTION_DRAG_ENDED;
        mExpected.add(new LogEntry(v, action, x, y, obtainClipData(action),
                obtainClipDescription(action), sLocalState, 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 = new int[2];
        release.getLocationOnScreen(releaseLoc);
        int action = DragEvent.ACTION_DRAG_ENDED;
        mExpected.add(new LogEntry(v, action,
                releaseLoc[0] + 6, releaseLoc[1] + 6, obtainClipData(action),
                obtainClipDescription(action), sLocalState, 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 = new int[2];
        v.getLocationOnScreen(viewLocation);
        int [] locationViewLocation = new int[2];
        locationView.getLocationOnScreen(locationViewLocation);
        mExpected.add(new LogEntry(v, action,
                locationViewLocation[0] - viewLocation[0] + offset,
                locationViewLocation[1] - viewLocation[1] + offset, obtainClipData(action),
                obtainClipDescription(action), sLocalState, 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 = new int [2];
            v.getLocationOnScreen(destLoc);
            long downTime = SystemClock.uptimeMillis();
            MotionEvent event = MotionEvent.obtain(downTime, downTime, action,
                    destLoc[0] + offset, destLoc[1] + offset, 1);
            event.setSource(InputDevice.SOURCE_MOUSE);
            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).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).equals(mExpected.get(i))) {
                    failWithLogs("Actual event #" + (i + 1) + " is different from expected");
                }
            }
        });
    }

    /** Checks if device type is watch. */
    private boolean isWatchDevice() {
        return mInstrumentation.getTargetContext().getPackageManager()
                .hasSystemFeature(PackageManager.FEATURE_WATCH);
    }

    @Before
    public void setUp() throws InterruptedException {
        assumeFalse(isWatchDevice());
        mActivity = startActivity(DragDropActivity.class);

        mStartReceived = new CountDownLatch(1);
        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) throws AssertionError {
        mMainThreadAssertionError = null;
        mInstrumentation.runOnMainSync(() -> {
            try {
                runner.run();
            } catch (AssertionError error) {
                mMainThreadAssertionError = error;
            }
        });
        if (mMainThreadAssertionError != null) {
            throw mMainThreadAssertionError;
        }
    }

    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(sClipData, new View.DragShadowBuilder(v), sLocalState, 0));
        });

        try {
            assertTrue("Timeout while waiting for START event",
                    mStartReceived.await(1, TimeUnit.SECONDS));
        } catch (InterruptedException e) {
            fail("Got InterruptedException while waiting for START event");
        }

        // This is needed after startDragAndDrop to ensure the drag window is ready.
        getInstrumentation().getUiAutomation().syncInputTransactions();
    }

    /**
     * Tests that no drag-drop events are sent to views that aren't supposed to receive them.
     */
    @Test
    public void testNoExtraEvents() throws Exception {
        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 {
        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 {
        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 {
        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 {
        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();
    }

    private boolean drawableStateContains(int resourceId, int attr) {
        return IntStream.of(mActivity.findViewById(resourceId).getDrawableState())
                .anyMatch(x -> x == attr);
    }

    /**
     * Tests that state_drag_hovered and state_drag_can_accept are set correctly.
     */
    @Test
    public void testDrawableState() throws Exception {
        runOnMain(() -> {
            // Set accepting handler for the inner view.
            mActivity.findViewById(R.id.inner).setOnDragListener((v, ev) -> {
                logEvent(v, ev);
                return true;
            });
            assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_can_accept));
        });

        startDrag();

        runOnMain(() -> {
            assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
            assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_can_accept));
        });

        // Move mouse into the view.
        injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE);
        runOnMain(() -> {
            assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
        });

        // Move out.
        injectMouse5(R.id.subcontainer, MotionEvent.ACTION_MOVE);
        runOnMain(() -> {
            assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
        });

        // Move in.
        injectMouse5(R.id.inner, MotionEvent.ACTION_MOVE);
        runOnMain(() -> {
            assertTrue(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
        });

        // Release there.
        injectMouse5(R.id.inner, MotionEvent.ACTION_UP);
        runOnMain(() -> {
            assertFalse(drawableStateContains(R.id.inner, android.R.attr.state_drag_hovered));
        });
    }

    /**
     * Tests if window is removing, it should not perform drag.
     */
    @Test
    public void testNoDragIfWindowCantReceiveInput() throws InterruptedException {
        injectMouse5(R.id.draggable, MotionEvent.ACTION_DOWN);

        runOnMain(() -> {
            // finish activity and start drag drop.
            View v = mActivity.findViewById(R.id.draggable);
            mActivity.finish();
            assertFalse("Shouldn't start drag",
                    v.startDragAndDrop(sClipData, new View.DragShadowBuilder(v), sLocalState, 0));
        });

        injectMouse5(R.id.draggable, MotionEvent.ACTION_UP);
    }

    /**
     * Tests if there is no touch down, it should not perform drag.
     */
    @Test
    public void testNoDragIfNoTouchDown() throws InterruptedException {
        // perform a click.
        injectMouse5(R.id.draggable, MotionEvent.ACTION_DOWN);
        injectMouse5(R.id.draggable, MotionEvent.ACTION_UP);

        runOnMain(() -> {
            View v = mActivity.findViewById(R.id.draggable);
            assertFalse("Shouldn't start drag",
                v.startDragAndDrop(sClipData, new View.DragShadowBuilder(v), sLocalState, 0));
        });
    }

    /**
     * Tests that the canvas is hardware accelerated when the activity is hardware accelerated.
     */
    @Test
    public void testHardwareAcceleratedCanvas() throws InterruptedException {
        assertDragCanvasHwAcceleratedState(mActivity, true);
    }

    /**
     * Tests that the canvas is not hardware accelerated when the activity is not hardware
     * accelerated.
     */
    @Test
    public void testSoftwareCanvas() throws InterruptedException {
        SoftwareCanvasDragDropActivity activity =
                startActivity(SoftwareCanvasDragDropActivity.class);
        assertDragCanvasHwAcceleratedState(activity, false);
    }

    private void assertDragCanvasHwAcceleratedState(DragDropActivity activity,
            boolean expectedHwAccelerated) {
        CountDownLatch latch = new CountDownLatch(1);
        AtomicBoolean isCanvasHwAccelerated = new AtomicBoolean();
        runOnMain(() -> {
            View v = activity.findViewById(R.id.draggable);
            v.startDragAndDrop(sClipData, new View.DragShadowBuilder(v) {
                @Override
                public void onDrawShadow(Canvas canvas) {
                    isCanvasHwAccelerated.set(canvas.isHardwareAccelerated());
                    latch.countDown();
                }
            }, null, 0);
        });

        try {
            assertTrue("Timeout while waiting for canvas", latch.await(5, TimeUnit.SECONDS));
            assertTrue("Expected canvas hardware acceleration to be: " + expectedHwAccelerated,
                    expectedHwAccelerated == isCanvasHwAccelerated.get());
        } catch (InterruptedException e) {
            fail("Got InterruptedException while waiting for canvas");
        }
    }

    public static class DragDropActivity extends FocusableActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.drag_drop_layout);
        }
    }

    public static class SoftwareCanvasDragDropActivity extends DragDropActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.drag_drop_layout);
        }
    }
}
