/*
 * Copyright (C) 2017 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 com.android.server.wm;

import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.annotation.NonNull;
import android.content.ClipData;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.util.Slog;
import android.view.Display;
import android.view.IWindow;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.SurfaceSession;
import android.view.View;

import com.android.internal.util.Preconditions;
import com.android.server.input.InputWindowHandle;
import com.android.server.wm.WindowManagerInternal.IDragDropCallback;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Managing drag and drop operations initiated by View#startDragAndDrop.
 */
class DragDropController {
    private static final float DRAG_SHADOW_ALPHA_TRANSPARENT = .7071f;
    private static final long DRAG_TIMEOUT_MS = 5000;

    // Messages for Handler.
    static final int MSG_DRAG_END_TIMEOUT = 0;
    static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 1;
    static final int MSG_ANIMATION_END = 2;

    /**
     * Drag state per operation.
     * Needs a lock of {@code WindowManagerService#mWindowMap} to read this. Needs both locks of
     * {@code mWriteLock} and {@code WindowManagerService#mWindowMap} to update this.
     * The variable is cleared by {@code #onDragStateClosedLocked} which is invoked by DragState
     * itself, thus the variable can be null after calling DragState's methods.
     */
    private DragState mDragState;

    private WindowManagerService mService;
    private final Handler mHandler;

    /**
     * Callback which is used to sync drag state with the vendor-specific code.
     */
    @NonNull private AtomicReference<IDragDropCallback> mCallback = new AtomicReference<>(
            new IDragDropCallback() {});

    boolean dragDropActiveLocked() {
        return mDragState != null;
    }

    InputWindowHandle getInputWindowHandleLocked() {
        return mDragState.getInputWindowHandle();
    }

    void registerCallback(IDragDropCallback callback) {
        Preconditions.checkNotNull(callback);
        mCallback.set(callback);
    }

    DragDropController(WindowManagerService service, Looper looper) {
        mService = service;
        mHandler = new DragHandler(service, looper);
    }

    void sendDragStartedIfNeededLocked(WindowState window) {
        mDragState.sendDragStartedIfNeededLocked(window);
    }

    IBinder performDrag(SurfaceSession session, int callerPid, int callerUid, IWindow window,
            int flags, SurfaceControl surface, int touchSource, float touchX, float touchY,
            float thumbCenterX, float thumbCenterY, ClipData data) {
        if (DEBUG_DRAG) {
            Slog.d(TAG_WM, "perform drag: win=" + window + " surface=" + surface + " flags=" +
                            Integer.toHexString(flags) + " data=" + data);
        }

        final IBinder dragToken = new Binder();
        final boolean callbackResult = mCallback.get().prePerformDrag(window, dragToken,
                touchSource, touchX, touchY, thumbCenterX, thumbCenterY, data);
        try {
            synchronized (mService.mWindowMap) {
                try {
                    if (!callbackResult) {
                        Slog.w(TAG_WM, "IDragDropCallback rejects the performDrag request");
                        return null;
                    }

                    if (dragDropActiveLocked()) {
                        Slog.w(TAG_WM, "Drag already in progress");
                        return null;
                    }

                    final WindowState callingWin = mService.windowForClientLocked(
                            null, window, false);
                    if (callingWin == null) {
                        Slog.w(TAG_WM, "Bad requesting window " + window);
                        return null;  // !!! TODO: throw here?
                    }

                    // !!! TODO: if input is not still focused on the initiating window, fail
                    // the drag initiation (e.g. an alarm window popped up just as the application
                    // called performDrag()

                    // !!! TODO: extract the current touch (x, y) in screen coordinates.  That
                    // will let us eliminate the (touchX,touchY) parameters from the API.

                    // !!! FIXME: put all this heavy stuff onto the mHandler looper, as well as
                    // the actual drag event dispatch stuff in the dragstate

                    // !!! TODO(multi-display): support other displays

                    final DisplayContent displayContent = callingWin.getDisplayContent();
                    if (displayContent == null) {
                        Slog.w(TAG_WM, "display content is null");
                        return null;
                    }

                    final float alpha = (flags & View.DRAG_FLAG_OPAQUE) == 0 ?
                            DRAG_SHADOW_ALPHA_TRANSPARENT : 1;
                    final IBinder winBinder = window.asBinder();
                    IBinder token = new Binder();
                    mDragState = new DragState(mService, this, token, surface, flags, winBinder);
                    surface = null;
                    mDragState.mPid = callerPid;
                    mDragState.mUid = callerUid;
                    mDragState.mOriginalAlpha = alpha;
                    mDragState.mToken = dragToken;

                    final Display display = displayContent.getDisplay();
                    if (!mCallback.get().registerInputChannel(
                            mDragState, display, mService.mInputManager,
                            callingWin.mInputChannel)) {
                        Slog.e(TAG_WM, "Unable to transfer touch focus");
                        return null;
                    }

                    mDragState.mDisplayContent = displayContent;
                    mDragState.mData = data;
                    mDragState.broadcastDragStartedLocked(touchX, touchY);
                    mDragState.overridePointerIconLocked(touchSource);
                    // remember the thumb offsets for later
                    mDragState.mThumbOffsetX = thumbCenterX;
                    mDragState.mThumbOffsetY = thumbCenterY;

                    // Make the surface visible at the proper location
                    final SurfaceControl surfaceControl = mDragState.mSurfaceControl;
                    if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG_WM, ">>> OPEN TRANSACTION performDrag");

                    final SurfaceControl.Transaction transaction =
                            callingWin.getPendingTransaction();
                    transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha);
                    transaction.setPosition(
                            surfaceControl, touchX - thumbCenterX, touchY - thumbCenterY);
                    transaction.show(surfaceControl);
                    displayContent.reparentToOverlay(transaction, surfaceControl);
                    callingWin.scheduleAnimation();

                    if (SHOW_LIGHT_TRANSACTIONS) {
                        Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag");
                    }

                    mDragState.notifyLocationLocked(touchX, touchY);
                } finally {
                    if (surface != null) {
                        surface.release();
                    }
                    if (mDragState != null && !mDragState.isInProgress()) {
                        mDragState.closeLocked();
                    }
                }
            }
            return dragToken;    // success!
        } finally {
            mCallback.get().postPerformDrag();
        }
    }

    void reportDropResult(IWindow window, boolean consumed) {
        IBinder token = window.asBinder();
        if (DEBUG_DRAG) {
            Slog.d(TAG_WM, "Drop result=" + consumed + " reported by " + token);
        }

        mCallback.get().preReportDropResult(window, consumed);
        try {
            synchronized (mService.mWindowMap) {
                if (mDragState == null) {
                    // Most likely the drop recipient ANRed and we ended the drag
                    // out from under it.  Log the issue and move on.
                    Slog.w(TAG_WM, "Drop result given but no drag in progress");
                    return;
                }

                if (mDragState.mToken != token) {
                    // We're in a drag, but the wrong window has responded.
                    Slog.w(TAG_WM, "Invalid drop-result claim by " + window);
                    throw new IllegalStateException("reportDropResult() by non-recipient");
                }

                // The right window has responded, even if it's no longer around,
                // so be sure to halt the timeout even if the later WindowState
                // lookup fails.
                mHandler.removeMessages(MSG_DRAG_END_TIMEOUT, window.asBinder());
                WindowState callingWin = mService.windowForClientLocked(null, window, false);
                if (callingWin == null) {
                    Slog.w(TAG_WM, "Bad result-reporting window " + window);
                    return;  // !!! TODO: throw here?
                }

                mDragState.mDragResult = consumed;
                mDragState.endDragLocked();
            }
        } finally {
            mCallback.get().postReportDropResult();
        }
    }

    void cancelDragAndDrop(IBinder dragToken) {
        if (DEBUG_DRAG) {
            Slog.d(TAG_WM, "cancelDragAndDrop");
        }

        mCallback.get().preCancelDragAndDrop(dragToken);
        try {
            synchronized (mService.mWindowMap) {
                if (mDragState == null) {
                    Slog.w(TAG_WM, "cancelDragAndDrop() without prepareDrag()");
                    throw new IllegalStateException("cancelDragAndDrop() without prepareDrag()");
                }

                if (mDragState.mToken != dragToken) {
                    Slog.w(TAG_WM,
                            "cancelDragAndDrop() does not match prepareDrag()");
                    throw new IllegalStateException(
                            "cancelDragAndDrop() does not match prepareDrag()");
                }

                mDragState.mDragResult = false;
                mDragState.cancelDragLocked();
            }
        } finally {
            mCallback.get().postCancelDragAndDrop();
        }
    }

    /**
     * Handles motion events.
     * @param keepHandling Whether if the drag operation is continuing or this is the last motion
     *          event.
     * @param newX X coordinate value in dp in the screen coordinate
     * @param newY Y coordinate value in dp in the screen coordinate
     */
    void handleMotionEvent(boolean keepHandling, float newX, float newY) {
        synchronized (mService.mWindowMap) {
            if (!dragDropActiveLocked()) {
                // The drag has ended but the clean-up message has not been processed by
                // window manager. Drop events that occur after this until window manager
                // has a chance to clean-up the input handle.
                return;
            }

            if (keepHandling) {
                mDragState.notifyMoveLocked(newX, newY);
            } else {
                mDragState.notifyDropLocked(newX, newY);
            }
        }
    }

    void dragRecipientEntered(IWindow window) {
        if (DEBUG_DRAG) {
            Slog.d(TAG_WM, "Drag into new candidate view @ " + window.asBinder());
        }
    }

    void dragRecipientExited(IWindow window) {
        if (DEBUG_DRAG) {
            Slog.d(TAG_WM, "Drag from old candidate view @ " + window.asBinder());
        }
    }

    /**
     * Sends a message to the Handler managed by DragDropController.
     */
    void sendHandlerMessage(int what, Object arg) {
        mHandler.obtainMessage(what, arg).sendToTarget();
    }

    /**
     * Sends a timeout message to the Handler managed by DragDropController.
     */
    void sendTimeoutMessage(int what, Object arg) {
        mHandler.removeMessages(what, arg);
        final Message msg = mHandler.obtainMessage(what, arg);
        mHandler.sendMessageDelayed(msg, DRAG_TIMEOUT_MS);
    }

    /**
     * Notifies the current drag state is closed.
     */
    void onDragStateClosedLocked(DragState dragState) {
        if (mDragState != dragState) {
            Slog.wtf(TAG_WM, "Unknown drag state is closed");
            return;
        }
        mDragState = null;
    }

    private class DragHandler extends Handler {
        /**
         * Lock for window manager.
         */
        private final WindowManagerService mService;

        DragHandler(WindowManagerService service, Looper looper) {
            super(looper);
            mService = service;
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DRAG_END_TIMEOUT: {
                    final IBinder win = (IBinder) msg.obj;
                    if (DEBUG_DRAG) {
                        Slog.w(TAG_WM, "Timeout ending drag to win " + win);
                    }

                    synchronized (mService.mWindowMap) {
                        // !!! TODO: ANR the drag-receiving app
                        if (mDragState != null) {
                            mDragState.mDragResult = false;
                            mDragState.endDragLocked();
                        }
                    }
                    break;
                }

                case MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT: {
                    if (DEBUG_DRAG)
                        Slog.d(TAG_WM, "Drag ending; tearing down input channel");
                    final DragState.InputInterceptor interceptor =
                            (DragState.InputInterceptor) msg.obj;
                    if (interceptor == null) return;
                    synchronized (mService.mWindowMap) {
                        interceptor.tearDown();
                    }
                    break;
                }

                case MSG_ANIMATION_END: {
                    synchronized (mService.mWindowMap) {
                        if (mDragState == null) {
                            Slog.wtf(TAG_WM, "mDragState unexpectedly became null while " +
                                    "plyaing animation");
                            return;
                        }
                        mDragState.closeLocked();
                    }
                    break;
                }
            }
        }
    }
}
