/*
 * Copyright (C) 2006 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;

import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
import static android.window.WindowProviderService.isWindowProviderService;

import static com.android.window.flags.Flags.screenRecordingCallbacks;

import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UiContext;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Region;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.StrictMode;
import android.util.Log;
import android.window.ITaskFpsCallback;
import android.window.InputTransferToken;
import android.window.TaskFpsCallback;
import android.window.TrustedPresentationThresholds;
import android.window.WindowContext;
import android.window.WindowMetricsController;
import android.window.WindowProvider;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.IResultReceiver;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;

/**
 * Provides low-level communication with the system window manager for
 * operations that are bound to a particular context, display or parent window.
 * Instances of this object are sensitive to the compatibility info associated
 * with the running application.
 *
 * This object implements the {@link ViewManager} interface,
 * allowing you to add any View subclass as a top-level window on the screen.
 * Additional window manager specific layout parameters are defined for
 * control over how windows are displayed.  It also implements the {@link WindowManager}
 * interface, allowing you to control the displays attached to the device.
 *
 * <p>Applications will not normally use WindowManager directly, instead relying
 * on the higher-level facilities in {@link android.app.Activity} and
 * {@link android.app.Dialog}.
 *
 * <p>Even for low-level window manager access, it is almost never correct to use
 * this class.  For example, {@link android.app.Activity#getWindowManager}
 * provides a window manager for adding windows that are associated with that
 * activity -- the window manager will not normally allow you to add arbitrary
 * windows that are not associated with an activity.
 *
 * @see WindowManager
 * @see WindowManagerGlobal
 * @hide
 */
public class WindowManagerImpl implements WindowManager {
    private static final String TAG = "WindowManager";

    @UnsupportedAppUsage
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    @UiContext
    @VisibleForTesting
    public final Context mContext;
    private final Window mParentWindow;

    /**
     * If {@link LayoutParams#token} is {@code null} and no parent window is specified, the value
     * of {@link LayoutParams#token} will be overridden to {@code mDefaultToken}.
     */
    private IBinder mDefaultToken;

    /**
     * This token will be set to {@link LayoutParams#mWindowContextToken} and used to receive
     * configuration changes from the server side.
     */
    @Nullable
    private final IBinder mWindowContextToken;

    @GuardedBy("mOnFpsCallbackListenerProxies")
    private final ArrayList<OnFpsCallbackListenerProxy> mOnFpsCallbackListenerProxies =
            new ArrayList<>();

    /** A controller to handle {@link WindowMetrics} related APIs */
    @NonNull
    private final WindowMetricsController mWindowMetricsController;

    public WindowManagerImpl(Context context) {
        this(context, null /* parentWindow */, null /* clientToken */);
    }

    public WindowManagerImpl(Context context, Window parentWindow,
            @Nullable IBinder windowContextToken) {
        mContext = context;
        mParentWindow = parentWindow;
        mWindowContextToken = windowContextToken;
        mWindowMetricsController = new WindowMetricsController(mContext);
    }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow, mWindowContextToken);
    }

    public WindowManagerImpl createPresentationWindowManager(Context displayContext) {
        return new WindowManagerImpl(displayContext, mParentWindow, mWindowContextToken);
    }

    /** Creates a {@link WindowManager} for a {@link WindowContext}. */
    public static WindowManager createWindowContextWindowManager(Context context) {
        final IBinder clientToken = context.getWindowContextToken();
        return new WindowManagerImpl(context, null /* parentWindow */, clientToken);
    }

    /**
     * Sets the window token to assign when none is specified by the client or
     * available from the parent window.
     *
     * @param token The default token to assign.
     */
    public void setDefaultToken(IBinder token) {
        mDefaultToken = token;
    }

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyTokens(params);
        mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
                mContext.getUserId());
    }

    @Override
    public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyTokens(params);
        mGlobal.updateViewLayout(view, params);
    }

    private void applyTokens(@NonNull ViewGroup.LayoutParams params) {
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        assertWindowContextTypeMatches(wparams.type);
        // Only use the default token if we don't have a parent window and a token.
        if (mDefaultToken != null && mParentWindow == null && wparams.token == null) {
            wparams.token = mDefaultToken;
        }
        wparams.mWindowContextToken = mWindowContextToken;
    }

    private void assertWindowContextTypeMatches(@LayoutParams.WindowType int windowType) {
        if (!(mContext instanceof WindowProvider)) {
            return;
        }
        // Don't need to check sub-window type because sub window should be allowed to be attached
        // to the parent window.
        if (windowType >= FIRST_SUB_WINDOW && windowType <= LAST_SUB_WINDOW) {
            return;
        }
        final WindowProvider windowProvider = (WindowProvider) mContext;
        if (windowProvider.getWindowType() == windowType) {
            return;
        }
        IllegalArgumentException exception = new IllegalArgumentException("Window type mismatch."
                + " Window Context's window type is " + windowProvider.getWindowType()
                + ", while LayoutParams' type is set to " + windowType + "."
                + " Please create another Window Context via"
                + " createWindowContext(getDisplay(), " + windowType + ", null)"
                + " to add window with type:" + windowType);
        if (!isWindowProviderService(windowProvider.getWindowContextOptions())) {
            throw exception;
        }
        // Throw IncorrectCorrectViolation if the Window Context is allowed to provide multiple
        // window types. Usually it's because the Window Context is a WindowProviderService.
        StrictMode.onIncorrectContextUsed("WindowContext's window type must"
                + " match type in WindowManager.LayoutParams", exception);
    }

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }

    @Override
    public void requestAppKeyboardShortcuts(
            final KeyboardShortcutsReceiver receiver, int deviceId) {
        IResultReceiver resultReceiver = new IResultReceiver.Stub() {
            @Override
            public void send(int resultCode, Bundle resultData) throws RemoteException {
                List<KeyboardShortcutGroup> result =
                        resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY,
                                android.view.KeyboardShortcutGroup.class);
                receiver.onKeyboardShortcutsReceived(result);
            }
        };
        try {
            WindowManagerGlobal.getWindowManagerService()
                    .requestAppKeyboardShortcuts(resultReceiver, deviceId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @Override
    public KeyboardShortcutGroup getApplicationLaunchKeyboardShortcuts(int deviceId) {
        try {
            return WindowManagerGlobal.getWindowManagerService()
                    .getApplicationLaunchKeyboardShortcuts(deviceId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @Override
    public void requestImeKeyboardShortcuts(
            final KeyboardShortcutsReceiver receiver, int deviceId) {
        IResultReceiver resultReceiver = new IResultReceiver.Stub() {
            @Override
            public void send(int resultCode, Bundle resultData) throws RemoteException {
                List<KeyboardShortcutGroup> result =
                        resultData.getParcelableArrayList(PARCEL_KEY_SHORTCUTS_ARRAY,
                                android.view.KeyboardShortcutGroup.class);
                receiver.onKeyboardShortcutsReceived(result);
            }
        };
        try {
            WindowManagerGlobal.getWindowManagerService()
                    .requestImeKeyboardShortcuts(resultReceiver, deviceId);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @Override
    public Display getDefaultDisplay() {
        return mContext.getDisplayNoVerify();
    }

    @Override
    public Region getCurrentImeTouchRegion() {
        try {
            return WindowManagerGlobal.getWindowManagerService().getCurrentImeTouchRegion();
        } catch (RemoteException e) {
        }
        return null;
    }

    @Override
    public void setShouldShowWithInsecureKeyguard(int displayId, boolean shouldShow) {
        try {
            WindowManagerGlobal.getWindowManagerService()
                    .setShouldShowWithInsecureKeyguard(displayId, shouldShow);
        } catch (RemoteException e) {
        }
    }

    @Override
    public void setShouldShowSystemDecors(int displayId, boolean shouldShow) {
        try {
            WindowManagerGlobal.getWindowManagerService()
                    .setShouldShowSystemDecors(displayId, shouldShow);
        } catch (RemoteException e) {
        }
    }

    @Override
    public boolean shouldShowSystemDecors(int displayId) {
        try {
            return WindowManagerGlobal.getWindowManagerService().shouldShowSystemDecors(displayId);
        } catch (RemoteException e) {
        }
        return false;
    }

    @Override
    public void setDisplayImePolicy(int displayId, @DisplayImePolicy int imePolicy) {
        try {
            WindowManagerGlobal.getWindowManagerService().setDisplayImePolicy(displayId, imePolicy);
        } catch (RemoteException e) {
        }
    }

    @Override
    public @DisplayImePolicy int getDisplayImePolicy(int displayId) {
        try {
            return WindowManagerGlobal.getWindowManagerService().getDisplayImePolicy(displayId);
        } catch (RemoteException e) {
        }
        return DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
    }

    @Override
    public boolean isGlobalKey(int keyCode) {
        try {
            return WindowManagerGlobal.getWindowManagerService().isGlobalKey(keyCode);
        } catch (RemoteException e) {
        }
        return false;
    }

    @Override
    public WindowMetrics getCurrentWindowMetrics() {
        return mWindowMetricsController.getCurrentWindowMetrics();
    }

    @Override
    public WindowMetrics getMaximumWindowMetrics() {
        return mWindowMetricsController.getMaximumWindowMetrics();
    }

    @Override
    @NonNull
    public Set<WindowMetrics> getPossibleMaximumWindowMetrics(int displayId) {
        return mWindowMetricsController.getPossibleMaximumWindowMetrics(displayId);
    }

    @Override
    public void holdLock(IBinder token, int durationMs) {
        try {
            WindowManagerGlobal.getWindowManagerService().holdLock(token, durationMs);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @Override
    public boolean isCrossWindowBlurEnabled() {
        return CrossWindowBlurListeners.getInstance().isCrossWindowBlurEnabled();
    }

    @Override
    public void addCrossWindowBlurEnabledListener(@NonNull Consumer<Boolean> listener) {
        addCrossWindowBlurEnabledListener(mContext.getMainExecutor(), listener);
    }

    @Override
    public void addCrossWindowBlurEnabledListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<Boolean> listener) {
        CrossWindowBlurListeners.getInstance().addListener(executor, listener);
    }

    @Override
    public void removeCrossWindowBlurEnabledListener(@NonNull Consumer<Boolean> listener) {
        CrossWindowBlurListeners.getInstance().removeListener(listener);
    }

    @Override
    public void addProposedRotationListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull IntConsumer listener) {
        Objects.requireNonNull(executor, "executor must not be null");
        Objects.requireNonNull(listener, "listener must not be null");
        final IBinder contextToken = Context.getToken(mContext);
        if (contextToken == null) {
            throw new UnsupportedOperationException("The context of this window manager instance "
                    + "must be a UI context, e.g. an Activity or a Context created by "
                    + "Context#createWindowContext()");
        }
        mGlobal.registerProposedRotationListener(contextToken, executor, listener);
    }

    @Override
    public void removeProposedRotationListener(@NonNull IntConsumer listener) {
        mGlobal.unregisterProposedRotationListener(Context.getToken(mContext), listener);
    }

    @Override
    public boolean isTaskSnapshotSupported() {
        try {
            return WindowManagerGlobal.getWindowManagerService().isTaskSnapshotSupported();
        } catch (RemoteException e) {
        }
        return false;
    }

    @Override
    public void registerTaskFpsCallback(@IntRange(from = 0) int taskId, @NonNull Executor executor,
            TaskFpsCallback callback) {
        final OnFpsCallbackListenerProxy onFpsCallbackListenerProxy =
                new OnFpsCallbackListenerProxy(executor, callback);
        try {
            WindowManagerGlobal.getWindowManagerService().registerTaskFpsCallback(
                    taskId, onFpsCallbackListenerProxy);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        synchronized (mOnFpsCallbackListenerProxies) {
            mOnFpsCallbackListenerProxies.add(onFpsCallbackListenerProxy);
        }
    }

    @Override
    public void unregisterTaskFpsCallback(TaskFpsCallback callback) {
        synchronized (mOnFpsCallbackListenerProxies) {
            final Iterator<OnFpsCallbackListenerProxy> iterator =
                    mOnFpsCallbackListenerProxies.iterator();
            while (iterator.hasNext()) {
                final OnFpsCallbackListenerProxy proxy = iterator.next();
                if (proxy.mCallback == callback) {
                    try {
                        WindowManagerGlobal.getWindowManagerService()
                                .unregisterTaskFpsCallback(proxy);
                    } catch (RemoteException e) {
                        throw e.rethrowFromSystemServer();
                    }
                    iterator.remove();
                }
            }
        }
    }

    private static class OnFpsCallbackListenerProxy
            extends ITaskFpsCallback.Stub {
        private final Executor mExecutor;
        private final TaskFpsCallback mCallback;

        private OnFpsCallbackListenerProxy(Executor executor, TaskFpsCallback callback) {
            mExecutor = executor;
            mCallback = callback;
        }

        @Override
        public void onFpsReported(float fps) {
            mExecutor.execute(() -> {
                mCallback.onFpsReported(fps);
            });
        }
    }

    @Override
    public Bitmap snapshotTaskForRecents(int taskId) {
        try {
            return WindowManagerGlobal.getWindowManagerService().snapshotTaskForRecents(taskId);
        } catch (RemoteException e) {
            e.rethrowAsRuntimeException();
        }
        return null;
    }

    @Override
    @NonNull
    public IBinder getDefaultToken() {
        return mDefaultToken;
    }

    @Override
    @NonNull
    public List<ComponentName> notifyScreenshotListeners(int displayId) {
        try {
            return List.copyOf(WindowManagerGlobal.getWindowManagerService()
                    .notifyScreenshotListeners(displayId));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @Override
    public boolean replaceContentOnDisplayWithMirror(int displayId, @NonNull Window window) {
        View decorView = window.peekDecorView();
        if (decorView == null) {
            Log.e(TAG, "replaceContentOnDisplayWithMirror: Window's decorView was null.");
            return false;
        }

        ViewRootImpl viewRoot = decorView.getViewRootImpl();
        if (viewRoot == null) {
            Log.e(TAG, "replaceContentOnDisplayWithMirror: Window's viewRootImpl was null.");
            return false;
        }

        SurfaceControl sc = viewRoot.getSurfaceControl();
        if (!sc.isValid()) {
            Log.e(TAG, "replaceContentOnDisplayWithMirror: Window's SC is invalid.");
            return false;
        }
        return replaceContentOnDisplayWithSc(displayId, SurfaceControl.mirrorSurface(sc));
    }

    @Override
    public boolean replaceContentOnDisplayWithSc(int displayId, @NonNull SurfaceControl sc) {
        if (!sc.isValid()) {
            Log.e(TAG, "replaceContentOnDisplayWithSc: Invalid SC.");
            return false;
        }

        try {
            return WindowManagerGlobal.getWindowManagerService()
                    .replaceContentOnDisplay(displayId, sc);
        } catch (RemoteException e) {
            e.rethrowAsRuntimeException();
        }
        return false;
    }

    @Override
    public void registerTrustedPresentationListener(@NonNull IBinder window,
            @NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor,
            @NonNull Consumer<Boolean> listener) {
        Objects.requireNonNull(window, "window must not be null");
        Objects.requireNonNull(thresholds, "thresholds must not be null");
        Objects.requireNonNull(executor, "executor must not be null");
        Objects.requireNonNull(listener, "listener must not be null");
        mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener);
    }

    @Override
    public void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) {
        Objects.requireNonNull(listener, "listener must not be null");
        mGlobal.unregisterTrustedPresentationListener(listener);
    }

    @NonNull
    @Override
    public InputTransferToken registerBatchedSurfaceControlInputReceiver(
            @NonNull InputTransferToken hostInputTransferToken,
            @NonNull SurfaceControl surfaceControl, @NonNull Choreographer choreographer,
            @NonNull SurfaceControlInputReceiver receiver) {
        Objects.requireNonNull(hostInputTransferToken);
        Objects.requireNonNull(surfaceControl);
        Objects.requireNonNull(choreographer);
        Objects.requireNonNull(receiver);
        return mGlobal.registerBatchedSurfaceControlInputReceiver(hostInputTransferToken,
                surfaceControl, choreographer, receiver);
    }

    @NonNull
    @Override
    public InputTransferToken registerUnbatchedSurfaceControlInputReceiver(
            @NonNull InputTransferToken hostInputTransferToken,
            @NonNull SurfaceControl surfaceControl, @NonNull Looper looper,
            @NonNull SurfaceControlInputReceiver receiver) {
        Objects.requireNonNull(hostInputTransferToken);
        Objects.requireNonNull(surfaceControl);
        Objects.requireNonNull(looper);
        Objects.requireNonNull(receiver);
        return mGlobal.registerUnbatchedSurfaceControlInputReceiver(
                hostInputTransferToken, surfaceControl, looper, receiver);
    }

    @Override
    public void unregisterSurfaceControlInputReceiver(@NonNull SurfaceControl surfaceControl) {
        Objects.requireNonNull(surfaceControl);
        mGlobal.unregisterSurfaceControlInputReceiver(surfaceControl);
    }

    @Override
    @Nullable
    public IBinder getSurfaceControlInputClientToken(@NonNull SurfaceControl surfaceControl) {
        Objects.requireNonNull(surfaceControl);
        return mGlobal.getSurfaceControlInputClientToken(surfaceControl);
    }

    @Override
    public boolean transferTouchGesture(@NonNull InputTransferToken transferFromToken,
            @NonNull InputTransferToken transferToToken) {
        Objects.requireNonNull(transferFromToken);
        Objects.requireNonNull(transferToToken);
        return mGlobal.transferTouchGesture(transferFromToken, transferToToken);
    }

    @Override
    public @ScreenRecordingState int addScreenRecordingCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        if (screenRecordingCallbacks()) {
            Objects.requireNonNull(executor, "executor must not be null");
            Objects.requireNonNull(callback, "callback must not be null");
            return ScreenRecordingCallbacks.getInstance().addCallback(executor, callback);
        }
        return SCREEN_RECORDING_STATE_NOT_VISIBLE;
    }

    @Override
    public void removeScreenRecordingCallback(
            @NonNull Consumer<@ScreenRecordingState Integer> callback) {
        if (screenRecordingCallbacks()) {
            Objects.requireNonNull(callback, "callback must not be null");
            ScreenRecordingCallbacks.getInstance().removeCallback(callback);
        }
    }
}
