/*
 * 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 android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.server.wm.CliIntentExtra.extraString;
import static android.server.wm.UiDeviceUtils.dragPointer;
import static android.server.wm.dndsourceapp.Components.DRAG_SOURCE;
import static android.server.wm.dndtargetapp.Components.DROP_TARGET;
import static android.server.wm.dndtargetappsdk23.Components.DROP_TARGET_SDK23;
import static android.view.Display.DEFAULT_DISPLAY;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

import android.content.ComponentName;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
import android.platform.test.annotations.AppModeFull;
import android.platform.test.annotations.Presubmit;
import android.server.wm.WindowManagerState.ActivityTask;
import android.util.Log;
import android.view.Display;

import com.google.common.collect.ImmutableSet;

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

import java.util.Map;

/**
 * Build/Install/Run:
 *     atest CtsWindowManagerDeviceTestCases:CrossAppDragAndDropTests
 */
@Presubmit
@AppModeFull(reason = "Requires android.permission.MANAGE_ACTIVITY_TASKS")
public class CrossAppDragAndDropTests extends ActivityManagerTestBase {
    private static final String TAG = "CrossAppDragAndDrop";

    private static final int SWIPE_STEPS = 100;

    private static final String FILE_GLOBAL = "file_global";
    private static final String FILE_LOCAL = "file_local";
    private static final String DISALLOW_GLOBAL = "disallow_global";
    private static final String CANCEL_SOON = "cancel_soon";
    private static final String GRANT_NONE = "grant_none";
    private static final String GRANT_READ = "grant_read";
    private static final String GRANT_WRITE = "grant_write";
    private static final String GRANT_READ_PREFIX = "grant_read_prefix";
    private static final String GRANT_READ_NOPREFIX = "grant_read_noprefix";
    private static final String GRANT_READ_PERSISTABLE = "grant_read_persistable";

    private static final String REQUEST_NONE = "request_none";
    private static final String REQUEST_READ = "request_read";
    private static final String REQUEST_READ_NESTED = "request_read_nested";
    private static final String REQUEST_TAKE_PERSISTABLE = "request_take_persistable";
    private static final String REQUEST_WRITE = "request_write";

    private static final String TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW =
            "textview_on_receive_content_listener";
    private static final String TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT =
            "edittext_on_receive_content_listener";
    private static final String TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT =
            "linearlayout_on_receive_content_listener";
    private static final ImmutableSet<String> ON_RECEIVE_CONTENT_LISTENER_MODES = ImmutableSet.of(
            TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW,
            TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT,
            TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT
    );

    private static final String SOURCE_LOG_TAG = "DragSource";
    private static final String TARGET_LOG_TAG = "DropTarget";

    private static final String RESULT_KEY_START_DRAG = "START_DRAG";
    private static final String RESULT_KEY_DRAG_STARTED = "DRAG_STARTED";
    private static final String RESULT_KEY_DRAG_ENDED = "DRAG_ENDED";
    private static final String RESULT_KEY_EXTRAS = "EXTRAS";
    private static final String RESULT_KEY_DROP_RESULT = "DROP";
    private static final String RESULT_KEY_ACCESS_BEFORE = "BEFORE";
    private static final String RESULT_KEY_ACCESS_AFTER = "AFTER";
    private static final String RESULT_KEY_CLIP_DATA_ERROR = "CLIP_DATA_ERROR";
    private static final String RESULT_KEY_CLIP_DESCR_ERROR = "CLIP_DESCR_ERROR";
    private static final String RESULT_KEY_LOCAL_STATE_ERROR = "LOCAL_STATE_ERROR";

    private static final String RESULT_MISSING = "Missing";
    private static final String RESULT_OK = "OK";
    private static final String RESULT_EXCEPTION = "Exception";
    private static final String RESULT_NULL_DROP_PERMISSIONS = "Null DragAndDropPermissions";

    private static final String EXTRA_MODE = "mode";
    private static final String EXTRA_LOGTAG = "logtag";

    private Map<String, String> mSourceResults;
    private Map<String, String> mTargetResults;

    private String mSessionId;
    private String mSourceLogTag;
    private String mTargetLogTag;

    @Before
    @Override
    public void setUp() throws Exception {
        super.setUp();
        assumeTrue(supportsSplitScreenMultiWindow() || supportsFreeform());

        // Use uptime in seconds as unique test invocation id.
        mSessionId = Long.toString(SystemClock.uptimeMillis() / 1000);
        mSourceLogTag = SOURCE_LOG_TAG + mSessionId;
        mTargetLogTag = TARGET_LOG_TAG + mSessionId;

        cleanupState();
    }

    @After
    public void tearDown() {
        cleanupState();
    }

    /**
     * Make sure that the special activity stacks are removed and the ActivityManager/WindowManager
     * is in a good state.
     */
    private void cleanupState() {
        stopTestPackage(DRAG_SOURCE.getPackageName());
        stopTestPackage(DROP_TARGET.getPackageName());
        stopTestPackage(DROP_TARGET_SDK23.getPackageName());
    }

    /**
     * @param displaySize size of the display
     * @param leftSide {@code true} to launch the app taking up the left half of the display,
     *         {@code false} to launch the app taking up the right half of the display.
     */
    private void launchFreeformActivity(ComponentName componentName, String mode,
            String logtag, Point displaySize, boolean leftSide) throws Exception {
        launchActivity(componentName, WINDOWING_MODE_FREEFORM, extraString("mode", mode),
                extraString("logtag", logtag));
        Point topLeft = new Point(leftSide ? 0 : displaySize.x / 2, 0);
        Point bottomRight = new Point(leftSide ? displaySize.x / 2 : displaySize.x, displaySize.y);
        resizeActivityTask(componentName, topLeft.x, topLeft.y, bottomRight.x, bottomRight.y);
        waitAndAssertTopResumedActivity(componentName, DEFAULT_DISPLAY,
                "Activity launched as freeform should be resumed");
    }

    private void injectInput(Point from, Point to, int steps) throws Exception {
        dragPointer(from, to, steps);
    }

    private Point getDisplaySize() throws Exception {
        final Point displaySize = new Point();
        mDm.getDisplay(Display.DEFAULT_DISPLAY).getRealSize(displaySize);
        return displaySize;
    }

    private Point getWindowCenter(ComponentName name) throws Exception {
        final ActivityTask sideTask = mWmState.getTaskByActivity(name);
        Rect bounds = sideTask.getBounds();
        if (bounds != null) {
            return new Point(bounds.centerX(), bounds.centerY());
        }
        return null;
    }

    private void assertDropResult(String sourceMode, String targetMode, String expectedDropResult)
            throws Exception {
        assertDragAndDropResults(DRAG_SOURCE, sourceMode, DROP_TARGET, targetMode,
                RESULT_OK, expectedDropResult, RESULT_OK);
    }

    private void assertNoGlobalDragEvents(ComponentName sourceComponentName, String sourceMode,
            ComponentName targetComponentName, String expectedStartDragResult)
            throws Exception {
        assertDragAndDropResults(
                sourceComponentName, sourceMode, targetComponentName, REQUEST_NONE,
                expectedStartDragResult, RESULT_MISSING, RESULT_MISSING);
    }

    private void assertDragAndDropResults(ComponentName sourceComponentName, String sourceMode,
            ComponentName targetComponentName, String targetMode,
            String expectedStartDragResult, String expectedDropResult,
            String expectedListenerResults) throws Exception {
        Log.e(TAG, "session: " + mSessionId + ", source: " + sourceMode
                + ", target: " + targetMode);

        if (supportsFreeform()) {
            // Fallback to try to launch two freeform windows side by side.
            Point displaySize = getDisplaySize();
            launchFreeformActivity(sourceComponentName, sourceMode, mSourceLogTag,
                displaySize, true /* leftSide */);
            launchFreeformActivity(targetComponentName, targetMode, mTargetLogTag,
                displaySize, false /* leftSide */);
        } else {
            // Launch primary activity.
            getLaunchActivityBuilder()
                    .setTargetActivity(sourceComponentName)
                    .setUseInstrumentation()
                    .setWaitForLaunched(true)
                    .setIntentExtra(bundle -> {
                        bundle.putString(EXTRA_MODE, sourceMode);
                        bundle.putString(EXTRA_LOGTAG, mSourceLogTag);
                    }).execute();

            // Launch secondary activity.
            getLaunchActivityBuilder().setTargetActivity(targetComponentName)
                    .setUseInstrumentation()
                    .setWaitForLaunched(true)
                    .setIntentExtra(bundle -> {
                        bundle.putString(EXTRA_MODE, targetMode);
                        bundle.putString(EXTRA_LOGTAG, mTargetLogTag);
                    }).execute();
            moveActivitiesToSplitScreen(sourceComponentName, targetComponentName);
        }

        Point p1 = getWindowCenter(sourceComponentName);
        assertNotNull(p1);
        Point p2 = getWindowCenter(targetComponentName);
        assertNotNull(p2);

        TestLogService.registerClient(mSourceLogTag, RESULT_KEY_START_DRAG);
        TestLogService.registerClient(mTargetLogTag, RESULT_KEY_DRAG_ENDED);

        injectInput(p1, p2, SWIPE_STEPS);

        mSourceResults = TestLogService.getResultsForClient(mSourceLogTag, 1000);
        assertSourceResult(RESULT_KEY_START_DRAG, expectedStartDragResult);

        mTargetResults = TestLogService.getResultsForClient(mTargetLogTag, 1000);
        assertTargetResult(RESULT_KEY_DROP_RESULT, expectedDropResult);

        // Skip the following assertions when testing OnReceiveContentListener, since it only
        // handles drop events.
        if (!ON_RECEIVE_CONTENT_LISTENER_MODES.contains(targetMode)) {
            if (!RESULT_MISSING.equals(expectedDropResult)) {
                assertTargetResult(RESULT_KEY_ACCESS_BEFORE, RESULT_EXCEPTION);
                assertTargetResult(RESULT_KEY_ACCESS_AFTER, RESULT_EXCEPTION);
            }
            assertListenerResults(expectedListenerResults);
        }
    }

    private void assertListenerResults(String expectedResult) throws Exception {
        assertTargetResult(RESULT_KEY_DRAG_STARTED, expectedResult);
        assertTargetResult(RESULT_KEY_DRAG_ENDED, expectedResult);
        assertTargetResult(RESULT_KEY_EXTRAS, expectedResult);

        assertTargetResult(RESULT_KEY_CLIP_DATA_ERROR, RESULT_MISSING);
        assertTargetResult(RESULT_KEY_CLIP_DESCR_ERROR, RESULT_MISSING);
        assertTargetResult(RESULT_KEY_LOCAL_STATE_ERROR, RESULT_MISSING);
    }

    private void assertSourceResult(String resultKey, String expectedResult) throws Exception {
        assertResult(mSourceResults, resultKey, expectedResult);
    }

    private void assertTargetResult(String resultKey, String expectedResult) throws Exception {
        assertResult(mTargetResults, resultKey, expectedResult);
    }

    private void assertResult(Map<String, String> results, String resultKey, String expectedResult)
            throws Exception {
        if (RESULT_MISSING.equals(expectedResult)) {
            if (results.containsKey(resultKey)) {
                fail("Unexpected " + resultKey + "=" + results.get(resultKey));
            }
        } else {
            assertTrue("Missing " + resultKey, results.containsKey(resultKey));
            assertEquals(resultKey + " result mismatch,", expectedResult,
                    results.get(resultKey));
        }
    }

    @Test
    public void testCancelSoon() throws Exception {
        assertDropResult(CANCEL_SOON, REQUEST_NONE, RESULT_MISSING);
    }

    @Test
    public void testDisallowGlobal() throws Exception {
        assertNoGlobalDragEvents(DRAG_SOURCE, DISALLOW_GLOBAL, DROP_TARGET, RESULT_OK);
    }

    @Test
    public void testDisallowGlobalBelowSdk24() throws Exception {
        assertNoGlobalDragEvents(DRAG_SOURCE, GRANT_NONE, DROP_TARGET_SDK23, RESULT_OK);
    }

    @Test
    public void testFileUriLocal() throws Exception {
        assertNoGlobalDragEvents(DRAG_SOURCE, FILE_LOCAL, DROP_TARGET, RESULT_OK);
    }

    @Test
    public void testFileUriGlobal() throws Exception {
        assertNoGlobalDragEvents(DRAG_SOURCE, FILE_GLOBAL, DROP_TARGET, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantNoneRequestNone() throws Exception {
        assertDropResult(GRANT_NONE, REQUEST_NONE, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantNoneRequestRead() throws Exception {
        assertDropResult(GRANT_NONE, REQUEST_READ, RESULT_NULL_DROP_PERMISSIONS);
    }

    @Test
    public void testGrantNoneRequestWrite() throws Exception {
        assertDropResult(GRANT_NONE, REQUEST_WRITE, RESULT_NULL_DROP_PERMISSIONS);
    }

    @Test
    public void testGrantReadRequestNone() throws Exception {
        assertDropResult(GRANT_READ, REQUEST_NONE, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantReadRequestRead() throws Exception {
        assertDropResult(GRANT_READ, REQUEST_READ, RESULT_OK);
    }

    @Test
    public void testGrantReadRequestWrite() throws Exception {
        assertDropResult(GRANT_READ, REQUEST_WRITE, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantReadNoPrefixRequestReadNested() throws Exception {
        assertDropResult(GRANT_READ_NOPREFIX, REQUEST_READ_NESTED, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantReadPrefixRequestReadNested() throws Exception {
        assertDropResult(GRANT_READ_PREFIX, REQUEST_READ_NESTED, RESULT_OK);
    }

    @Test
    public void testGrantPersistableRequestTakePersistable() throws Exception {
        assertDropResult(GRANT_READ_PERSISTABLE, REQUEST_TAKE_PERSISTABLE, RESULT_OK);
    }

    @Test
    public void testGrantReadRequestTakePersistable() throws Exception {
        assertDropResult(GRANT_READ, REQUEST_TAKE_PERSISTABLE, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantWriteRequestNone() throws Exception {
        assertDropResult(GRANT_WRITE, REQUEST_NONE, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantWriteRequestRead() throws Exception {
        assertDropResult(GRANT_WRITE, REQUEST_READ, RESULT_EXCEPTION);
    }

    @Test
    public void testGrantWriteRequestWrite() throws Exception {
        assertDropResult(GRANT_WRITE, REQUEST_WRITE, RESULT_OK);
    }

    @Test
    public void testOnReceiveContentListener_TextView_GrantRead() throws Exception {
        assertDropResult(GRANT_READ, TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW, RESULT_OK);
    }

    @Test
    public void testOnReceiveContentListener_TextView_GrantNone() throws Exception {
        assertDropResult(GRANT_NONE, TARGET_ON_RECEIVE_CONTENT_LISTENER_TEXT_VIEW,
                RESULT_EXCEPTION);
    }

    @Test
    public void testOnReceiveContentListener_EditText_GrantRead() throws Exception {
        assertDropResult(GRANT_READ, TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT, RESULT_OK);
    }

    @Test
    public void testOnReceiveContentListener_EditText_GrantNone() throws Exception {
        assertDropResult(GRANT_NONE, TARGET_ON_RECEIVE_CONTENT_LISTENER_EDIT_TEXT,
                RESULT_EXCEPTION);
    }

    @Test
    public void testOnReceiveContentListener_LinearLayout_GrantRead() throws Exception {
        assertDropResult(GRANT_READ, TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT, RESULT_OK);
    }

    @Test
    public void testOnReceiveContentListener_LinearLayout_GrantNone() throws Exception {
        assertDropResult(GRANT_NONE, TARGET_ON_RECEIVE_CONTENT_LISTENER_LINEAR_LAYOUT,
                RESULT_EXCEPTION);
    }
}
