/*
 * Copyright (C) 2022 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 android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.util.DisplayMetrics.DENSITY_DEFAULT;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;

import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.util.Size;
import android.view.Gravity;
import android.view.View;
import android.window.WindowContainerToken;

import java.util.function.Consumer;

/**
 * The static class that defines some utility constants and functions that are shared among launch
 * params modifiers.
 */
class LaunchParamsUtil {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "LaunchParamsUtil" : TAG_ATM;
    private static final boolean DEBUG = false;

    // Screen size of Nexus 5x
    static final int DEFAULT_PORTRAIT_FREEFORM_WIDTH_DP = 412;
    static final int DEFAULT_PORTRAIT_FREEFORM_HEIGHT_DP = 732;

    // One of the most common tablet sizes that are small enough to fit in most large screens.
    private static final int DEFAULT_LANDSCAPE_FREEFORM_WIDTH_DP = 1064;
    private static final int DEFAULT_LANDSCAPE_FREEFORM_HEIGHT_DP = 600;

    private static final int DISPLAY_EDGE_OFFSET_DP = 27;

    private static final Rect TMP_STABLE_BOUNDS = new Rect();

    private LaunchParamsUtil() {}

    /**
     * Gets centered bounds of width x height. If inOutBounds is not empty, the result bounds
     * centers at its center or displayArea's app bounds center if inOutBounds is empty.
     */
    static void centerBounds(@NonNull TaskDisplayArea displayArea, int width, int height,
            @NonNull Rect inOutBounds) {
        if (inOutBounds.isEmpty()) {
            displayArea.getStableRect(inOutBounds);
        }
        final int left = inOutBounds.centerX() - width / 2;
        final int top = inOutBounds.centerY() - height / 2;
        inOutBounds.set(left, top, left + width, top + height);
    }

    /**
     * Calculate the default size for a freeform environment. |defaultSize| is used as the default
     * DP size, but if this is null, the portrait phone size is used.
     */
    static Size getDefaultFreeformSize(@NonNull ActivityRecord activityRecord,
            @NonNull TaskDisplayArea displayArea,
            @NonNull ActivityInfo.WindowLayout layout, int orientation,
            @NonNull Rect stableBounds) {
        // Get window size based on Nexus 5x screen, we assume that this is enough to show content
        // of activities.
        final float density = (float) displayArea.getConfiguration().densityDpi / DENSITY_DEFAULT;
        final int freeformWidthInDp = (orientation == SCREEN_ORIENTATION_LANDSCAPE)
                ? DEFAULT_LANDSCAPE_FREEFORM_WIDTH_DP : DEFAULT_PORTRAIT_FREEFORM_WIDTH_DP;
        final int freeformHeightInDp = (orientation == SCREEN_ORIENTATION_LANDSCAPE)
                ? DEFAULT_LANDSCAPE_FREEFORM_HEIGHT_DP : DEFAULT_PORTRAIT_FREEFORM_HEIGHT_DP;
        final int freeformWidth = (int) (freeformWidthInDp * density + 0.5f);
        final int freeformHeight = (int) (freeformHeightInDp * density + 0.5f);

        // Minimum layout requirements.
        final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth;
        final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight;

        // Max size, which is letterboxing/pillarboxing in displayArea. That's to say the large
        // dimension of default size is the small dimension of displayArea size, and the small
        // dimension of default size is calculated to keep the same aspect ratio as the
        // displayArea's. Here we use stable bounds of displayArea because that indicates the area
        // that isn't occupied by system widgets (e.g. sysbar and navbar).
        final int portraitHeight = Math.min(stableBounds.width(), stableBounds.height());
        final int otherDimension = Math.max(stableBounds.width(), stableBounds.height());
        final int portraitWidth = (portraitHeight * portraitHeight) / otherDimension;
        final int maxWidth = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitHeight
                : portraitWidth;
        final int maxHeight = (orientation == SCREEN_ORIENTATION_LANDSCAPE) ? portraitWidth
                : portraitHeight;
        final int width = Math.min(maxWidth, Math.max(freeformWidth, layoutMinWidth));
        final int height = Math.min(maxHeight, Math.max(freeformHeight, layoutMinHeight));
        final float aspectRatio = (float) Math.max(width, height) / (float) Math.min(width, height);

        // Aspect ratio requirements.
        final float minAspectRatio = activityRecord.getMinAspectRatio();
        final float maxAspectRatio = activityRecord.info.getMaxAspectRatio();

        // Adjust the width and height to the aspect ratio requirements.
        int adjWidth = width;
        int adjHeight = height;
        if (minAspectRatio >= 1 && aspectRatio < minAspectRatio) {
            // The aspect ratio is below the minimum, adjust it to the minimum.
            if (orientation == SCREEN_ORIENTATION_LANDSCAPE) {
                // Fix the width, scale the height.
                adjHeight = (int) (adjWidth / minAspectRatio + 0.5f);
            } else {
                // Fix the height, scale the width.
                adjWidth = (int) (adjHeight / minAspectRatio + 0.5f);
            }
        } else if (maxAspectRatio >= 1 && aspectRatio > maxAspectRatio) {
            // The aspect ratio exceeds the maximum, adjust it to the maximum.
            if (orientation == SCREEN_ORIENTATION_LANDSCAPE) {
                // Fix the width, scale the height.
                adjHeight = (int) (adjWidth / maxAspectRatio + 0.5f);
            } else {
                // Fix the height, scale the width.
                adjWidth = (int) (adjHeight / maxAspectRatio + 0.5f);
            }
        }

        return new Size(adjWidth, adjHeight);
    }

    static void adjustBoundsToFitInDisplayArea(@NonNull TaskDisplayArea displayArea,
                                               int layoutDirection,
                                               @NonNull ActivityInfo.WindowLayout layout,
                                               @NonNull Rect inOutBounds) {
        // Give a small margin between the window bounds and the display bounds.
        final Rect stableBounds = TMP_STABLE_BOUNDS;
        displayArea.getStableRect(stableBounds);
        final float density = (float) displayArea.getConfiguration().densityDpi / DENSITY_DEFAULT;
        final int displayEdgeOffset = (int) (DISPLAY_EDGE_OFFSET_DP * density + 0.5f);
        stableBounds.inset(displayEdgeOffset, displayEdgeOffset);

        if (stableBounds.width() < inOutBounds.width()
                || stableBounds.height() < inOutBounds.height()) {
            final float heightShrinkRatio = stableBounds.width() / (float) inOutBounds.width();
            final float widthShrinkRatio =
                    stableBounds.height() / (float) inOutBounds.height();
            final float shrinkRatio = Math.min(heightShrinkRatio, widthShrinkRatio);
            // Minimum layout requirements.
            final int layoutMinWidth = (layout == null) ? -1 : layout.minWidth;
            final int layoutMinHeight = (layout == null) ? -1 : layout.minHeight;
            int adjustedWidth = Math.max(layoutMinWidth, (int) (inOutBounds.width() * shrinkRatio));
            int adjustedHeight = Math.max(layoutMinHeight,
                    (int) (inOutBounds.height() * shrinkRatio));
            if (stableBounds.width() < adjustedWidth
                    || stableBounds.height() < adjustedHeight) {
                // There is no way for us to fit the bounds in the displayArea without breaking min
                // size constraints. Set the min size to make visible as much content as possible.
                final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL
                        ? stableBounds.right - adjustedWidth
                        : stableBounds.left;
                inOutBounds.set(left, stableBounds.top, left + adjustedWidth,
                        stableBounds.top + adjustedHeight);
                return;
            }
            inOutBounds.set(inOutBounds.left, inOutBounds.top,
                    inOutBounds.left + adjustedWidth, inOutBounds.top + adjustedHeight);
        }

        final int dx;
        if (inOutBounds.right > stableBounds.right) {
            // Right edge is out of displayArea.
            dx = stableBounds.right - inOutBounds.right;
        } else if (inOutBounds.left < stableBounds.left) {
            // Left edge is out of displayArea.
            dx = stableBounds.left - inOutBounds.left;
        } else {
            // Vertical edges are all in displayArea.
            dx = 0;
        }

        final int dy;
        if (inOutBounds.top < stableBounds.top) {
            // Top edge is out of displayArea.
            dy = stableBounds.top - inOutBounds.top;
        } else if (inOutBounds.bottom > stableBounds.bottom) {
            // Bottom edge is out of displayArea.
            dy = stableBounds.bottom - inOutBounds.bottom;
        } else {
            // Horizontal edges are all in displayArea.
            dy = 0;
        }
        inOutBounds.offset(dx, dy);
    }

    /**
     * Calculates bounds based on window layout size manifest values. These can include width,
     * height, width fraction and height fraction. In the event only one dimension of values are
     * specified in the manifest (e.g. width but no height value), the corresponding display area
     * dimension will be used as the default value unless some desired sizes have been specified.
     */
    static void calculateLayoutBounds(@NonNull Rect stableBounds,
            @NonNull ActivityInfo.WindowLayout windowLayout, @NonNull Rect inOutBounds,
            @Nullable Size desiredSize) {
        final int defaultWidth = stableBounds.width();
        final int defaultHeight = stableBounds.height();
        int width;
        int height;

        if (desiredSize == null) {
            // If desired bounds have not been specified, use the exiting default bounds as the
            // desired.
            desiredSize = new Size(stableBounds.width(), stableBounds.height());
        }

        width = desiredSize.getWidth();
        if (windowLayout.width > 0 && windowLayout.width < defaultWidth) {
            width = windowLayout.width;
        } else if (windowLayout.widthFraction > 0 && windowLayout.widthFraction < 1.0f) {
            width = (int) (defaultWidth * windowLayout.widthFraction);
        }

        height = desiredSize.getHeight();
        if (windowLayout.height > 0 && windowLayout.height < defaultHeight) {
            height = windowLayout.height;
        } else if (windowLayout.heightFraction > 0 && windowLayout.heightFraction < 1.0f) {
            height = (int) (defaultHeight * windowLayout.heightFraction);
        }

        inOutBounds.set(0, 0, width, height);
    }

    /**
     * Applies a vertical and horizontal gravity on the inOutBounds in relation to the stableBounds.
     */
    static void applyLayoutGravity(int verticalGravity, int horizontalGravity,
            @NonNull Rect inOutBounds, @NonNull Rect stableBounds) {
        final int width = inOutBounds.width();
        final int height = inOutBounds.height();

        final float fractionOfHorizontalOffset;
        switch (horizontalGravity) {
            case Gravity.LEFT:
                fractionOfHorizontalOffset = 0f;
                break;
            case Gravity.RIGHT:
                fractionOfHorizontalOffset = 1f;
                break;
            default:
                fractionOfHorizontalOffset = 0.5f;
        }

        final float fractionOfVerticalOffset;
        switch (verticalGravity) {
            case Gravity.TOP:
                fractionOfVerticalOffset = 0f;
                break;
            case Gravity.BOTTOM:
                fractionOfVerticalOffset = 1f;
                break;
            default:
                fractionOfVerticalOffset = 0.5f;
        }

        inOutBounds.offsetTo(stableBounds.left, stableBounds.top);
        final int xOffset = (int) (fractionOfHorizontalOffset * (stableBounds.width() - width));
        final int yOffset = (int) (fractionOfVerticalOffset * (stableBounds.height() - height));
        inOutBounds.offset(xOffset, yOffset);
    }

    @NonNull
    static TaskDisplayArea getPreferredLaunchTaskDisplayArea(
            @NonNull ActivityTaskSupervisor supervisor, @Nullable Task task,
            @Nullable ActivityOptions options, @Nullable ActivityRecord source,
            @Nullable LaunchParamsController.LaunchParams currentParams,
            @Nullable ActivityRecord activityRecord, @Nullable ActivityStarter.Request request,
            @NonNull Consumer<String> logger) {
        TaskDisplayArea taskDisplayArea = null;

        final WindowContainerToken optionLaunchTaskDisplayAreaToken = options != null
                ? options.getLaunchTaskDisplayArea() : null;
        if (optionLaunchTaskDisplayAreaToken != null) {
            taskDisplayArea = (TaskDisplayArea) WindowContainer.fromBinder(
                    optionLaunchTaskDisplayAreaToken.asBinder());
            logger.accept("display-area-token-from-option=" + taskDisplayArea);
        }

        if (taskDisplayArea == null && options != null) {
            final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId();
            if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) {
                final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY
                        ? DEFAULT_DISPLAY : options.getLaunchDisplayId();
                final DisplayContent dc = supervisor.mRootWindowContainer
                        .getDisplayContent(launchDisplayId);
                if (dc != null) {
                    taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda ->
                            tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null);
                    logger.accept("display-area-feature-from-option=" + taskDisplayArea);
                }
            }
        }

        // If task display area is not specified in options - try display id
        if (taskDisplayArea == null) {
            final int optionLaunchId =
                    options != null ? options.getLaunchDisplayId() : INVALID_DISPLAY;
            if (optionLaunchId != INVALID_DISPLAY) {
                final DisplayContent dc = supervisor.mRootWindowContainer
                        .getDisplayContent(optionLaunchId);
                if (dc != null) {
                    taskDisplayArea = dc.getDefaultTaskDisplayArea();
                    logger.accept("display-from-option=" + optionLaunchId);
                }
            }
        }

        // If the source activity is a no-display activity, pass on the launch display area token
        // from source activity as currently preferred.
        if (taskDisplayArea == null && source != null && source.isNoDisplay()) {
            taskDisplayArea = source.mHandoverTaskDisplayArea;
            if (taskDisplayArea != null) {
                logger.accept("display-area-from-no-display-source=" + taskDisplayArea);
            } else {
                // Try handover display id
                final int displayId = source.mHandoverLaunchDisplayId;
                final DisplayContent dc =
                        supervisor.mRootWindowContainer.getDisplayContent(displayId);
                if (dc != null) {
                    taskDisplayArea = dc.getDefaultTaskDisplayArea();
                    logger.accept("display-from-no-display-source=" + displayId);
                }
            }
        }

        if (taskDisplayArea == null && source != null) {
            final TaskDisplayArea sourceDisplayArea = source.getDisplayArea();
            logger.accept("display-area-from-source=" + sourceDisplayArea);
            taskDisplayArea = sourceDisplayArea;
        }

        final Task rootTask = (taskDisplayArea == null && task != null)
                ? task.getRootTask() : null;
        if (rootTask != null) {
            logger.accept("display-from-task=" + rootTask.getDisplayId());
            taskDisplayArea = rootTask.getDisplayArea();
        }

        if (taskDisplayArea == null && options != null) {
            final int callerDisplayId = options.getCallerDisplayId();
            final DisplayContent dc =
                    supervisor.mRootWindowContainer.getDisplayContent(callerDisplayId);
            if (dc != null) {
                taskDisplayArea = dc.getDefaultTaskDisplayArea();
                logger.accept("display-from-caller=" + callerDisplayId);
            }
        }

        if (taskDisplayArea == null && currentParams != null) {
            taskDisplayArea = currentParams.mPreferredTaskDisplayArea;
            logger.accept("display-area-from-current-params=" + taskDisplayArea);
        }

        // Re-route to default display if the device didn't declare support for multi-display
        if (taskDisplayArea != null && !supervisor.mService.mSupportsMultiDisplay
                && taskDisplayArea.getDisplayId() != DEFAULT_DISPLAY) {
            taskDisplayArea = supervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
            logger.accept("display-area-from-no-multidisplay=" + taskDisplayArea);
        }

        // Re-route to default display if the home activity doesn't support multi-display
        if (taskDisplayArea != null && activityRecord != null && activityRecord.isActivityTypeHome()
                && !supervisor.mRootWindowContainer.canStartHomeOnDisplayArea(activityRecord.info,
                taskDisplayArea, false /* allowInstrumenting */)) {
            taskDisplayArea = supervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
            logger.accept("display-area-from-home=" + taskDisplayArea);
        }

        return (taskDisplayArea != null)
                ? taskDisplayArea
                : getFallbackDisplayAreaForActivity(activityRecord, request, supervisor, logger);
    }

    /**
     * Calculates the default {@link TaskDisplayArea} for a task. We attempt to put the activity
     * within the same display area if possible. The strategy is to find the display in the
     * following order:
     *
     * <ol>
     *     <li>The display area of the top activity from the launching process will be used</li>
     *     <li>The display area of the top activity from the real launching process will be used
     *     </li>
     *     <li>The default display area of the current focused display will be used.</li>
     * </ol>
     * @param activityRecord the activity being started
     * @param request optional {@link ActivityStarter.Request} made to start the activity record
     * @return {@link TaskDisplayArea} to house the task
     */
    @NonNull
    private static TaskDisplayArea getFallbackDisplayAreaForActivity(
            @Nullable ActivityRecord activityRecord, @Nullable ActivityStarter.Request request,
            @NonNull ActivityTaskSupervisor supervisor, @NonNull Consumer<String> logger) {
        if (activityRecord != null) {
            final WindowProcessController controllerFromLaunchingRecord =
                    supervisor.mService.getProcessController(
                            activityRecord.launchedFromPid, activityRecord.launchedFromUid);
            if (controllerFromLaunchingRecord != null) {
                final TaskDisplayArea taskDisplayAreaForLaunchingRecord =
                        controllerFromLaunchingRecord.getTopActivityDisplayArea();
                if (canPlaceEntityOnDisplay(taskDisplayAreaForLaunchingRecord, activityRecord,
                        request, supervisor)) {
                    logger.accept("display-area-for-launching-record="
                            + taskDisplayAreaForLaunchingRecord);
                    return taskDisplayAreaForLaunchingRecord;
                }
            }

            final WindowProcessController controllerFromProcess =
                    supervisor.mService.getProcessController(
                            activityRecord.getProcessName(), activityRecord.getUid());
            if (controllerFromProcess != null) {
                final TaskDisplayArea displayAreaForRecord =
                        controllerFromProcess.getTopActivityDisplayArea();
                if (canPlaceEntityOnDisplay(displayAreaForRecord, activityRecord,
                        request, supervisor)) {
                    logger.accept("display-area-for-record=" + displayAreaForRecord);
                    return displayAreaForRecord;
                }
            }
        }

        if (request != null) {
            final WindowProcessController controllerFromRequest =
                    supervisor.mService.getProcessController(
                            request.realCallingPid, request.realCallingUid);
            if (controllerFromRequest != null) {
                final TaskDisplayArea displayAreaFromSourceProcess =
                        controllerFromRequest.getTopActivityDisplayArea();
                if (canPlaceEntityOnDisplay(displayAreaFromSourceProcess, activityRecord,
                        request, supervisor)) {
                    logger.accept("display-area-source-process=" + displayAreaFromSourceProcess);
                    return displayAreaFromSourceProcess;
                }
            }
        }

        if (com.android.window.flags.Flags.fallbackToFocusedDisplay()) {
            // Select the TDA from the top focused display if possible.
            final DisplayContent focusedDisplay =
                    supervisor.mRootWindowContainer.getTopFocusedDisplayContent();
            final TaskDisplayArea defaultTaskDisplayArea;
            if (canPlaceEntityOnDisplay(focusedDisplay.getDefaultTaskDisplayArea(), activityRecord,
                    request, supervisor)) {
                defaultTaskDisplayArea = focusedDisplay.getDefaultTaskDisplayArea();
            } else {
                defaultTaskDisplayArea =
                        supervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
            }
            logger.accept("display-area-from-default-fallback=" + defaultTaskDisplayArea);
            return defaultTaskDisplayArea;
        } else {
            final TaskDisplayArea defaultTaskDisplayArea =
                    supervisor.mRootWindowContainer.getDefaultTaskDisplayArea();
            logger.accept("display-area-from-default-fallback=" + defaultTaskDisplayArea);
            return defaultTaskDisplayArea;
        }
    }

    /**
     * Returns {@code true} if the given activity can be placed on the TaskDisplayArea.
     */
    private static boolean canPlaceEntityOnDisplay(@Nullable TaskDisplayArea tda,
            @Nullable ActivityRecord activity, @Nullable ActivityStarter.Request request,
            @NonNull ActivityTaskSupervisor supervisor) {
        if (tda == null) {
            return false;
        }

        final int displayId = tda.getDisplayId();
        if (activity != null) {
            return activity.canBeLaunchedOnDisplay(displayId);
        }

        if (request != null && request.activityInfo != null) {
            return supervisor.canPlaceEntityOnDisplay(displayId, request.callingPid,
                    request.callingUid, request.activityInfo);
        }
        return false;
    }
}
