Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2199b806 authored by Eghosa Ewansiha-Vlachavas's avatar Eghosa Ewansiha-Vlachavas Committed by Android (Google) Code Review
Browse files

Merge "[1/n] Introduce dynamic bounds calculation to prevent letterboxing" into main

parents afa3ba52 b6a3d537
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -350,6 +350,12 @@ public class TaskInfo {
        return configuration.windowConfiguration.getWindowingMode();
    }

    /** @hide */
    public boolean isFreeform() {
        return configuration.windowConfiguration.getWindowingMode()
                == WindowConfiguration.WINDOWING_MODE_FREEFORM;
    }

    /** @hide */
    @WindowConfiguration.ActivityType
    public int getActivityType() {
+8 −0
Original line number Diff line number Diff line
@@ -81,6 +81,10 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
        super(context, taskInfo, syncQueue, taskListener, displayLayout);
        mCallback = callback;
        mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
        if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
            // Don't show the SCM button for freeform tasks
            mHasSizeCompat &= !taskInfo.isFreeform();
        }
        mCameraCompatControlState =
                taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;
        mCompatUIHintsState = compatUIHintsState;
@@ -136,6 +140,10 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract {
        final boolean prevHasSizeCompat = mHasSizeCompat;
        final int prevCameraCompatControlState = mCameraCompatControlState;
        mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
        if (Flags.enableDesktopWindowingMode() && Flags.enableWindowingDynamicInitialBounds()) {
            // Don't show the SCM button for freeform tasks
            mHasSizeCompat &= !taskInfo.isFreeform();
        }
        mCameraCompatControlState =
                taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.cameraCompatControlState;

+173 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.
 */

@file:JvmName("DesktopModeUtils")

package com.android.wm.shell.desktopmode

import android.app.ActivityManager.RunningTaskInfo
import android.content.pm.ActivityInfo.isFixedOrientationLandscape
import android.content.pm.ActivityInfo.isFixedOrientationPortrait
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.graphics.Rect
import android.os.SystemProperties
import android.util.Size
import com.android.wm.shell.common.DisplayLayout


val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float = SystemProperties
        .getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f

val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int = SystemProperties
        .getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)


/**
 * Calculates the initial bounds required for an application to fill a scale of the display bounds
 * without any letterboxing. This is done by taking into account the applications fullscreen size,
 * aspect ratio, orientation and resizability to calculate an area this is compatible with the
 * applications previous configuration.
 */
fun calculateInitialBounds(
    displayLayout: DisplayLayout,
    taskInfo: RunningTaskInfo,
    scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE
): Rect {
    val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
    val appAspectRatio = calculateAspectRatio(taskInfo)
    val idealSize = calculateIdealSize(screenBounds, scale)
    // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
    // Instead default to the desired initial bounds.
    val topActivityInfo = taskInfo.topActivityInfo
        ?: return positionInScreen(idealSize, screenBounds)

    val initialSize: Size = when (taskInfo.configuration.orientation) {
        ORIENTATION_LANDSCAPE -> {
            if (taskInfo.isResizeable) {
                if (isFixedOrientationPortrait(topActivityInfo.screenOrientation)) {
                    // Respect apps fullscreen width
                    Size(taskInfo.appCompatTaskInfo.topActivityLetterboxWidth, idealSize.height)
                } else {
                    idealSize
                }
            } else {
                maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
                    appAspectRatio)
            }
        }
        ORIENTATION_PORTRAIT -> {
            val customPortraitWidthForLandscapeApp = screenBounds.width() -
                    (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
            if (taskInfo.isResizeable) {
                if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
                    // Respect apps fullscreen height and apply custom app width
                    Size(customPortraitWidthForLandscapeApp,
                        taskInfo.appCompatTaskInfo.topActivityLetterboxHeight)
                } else {
                    idealSize
                }
            } else {
                if (isFixedOrientationLandscape(topActivityInfo.screenOrientation)) {
                    // Apply custom app width and calculate maximum size
                    maximumSizeMaintainingAspectRatio(
                        taskInfo,
                        Size(customPortraitWidthForLandscapeApp, idealSize.height),
                        appAspectRatio)
                } else {
                    maximumSizeMaintainingAspectRatio(taskInfo, idealSize,
                        appAspectRatio)
                }
            }
        }
        else -> {
            idealSize
        }
    }

    return positionInScreen(initialSize, screenBounds)
}

/**
 * Calculates the largest size that can fit in a given area while maintaining a specific aspect
 * ratio.
 */
private fun maximumSizeMaintainingAspectRatio(
    taskInfo: RunningTaskInfo,
    targetArea: Size,
    aspectRatio: Float
): Size {
    val targetHeight = targetArea.height
    val targetWidth = targetArea.width
    val finalHeight: Int
    val finalWidth: Int
    if (isFixedOrientationPortrait(taskInfo.topActivityInfo!!.screenOrientation)) {
        val tempWidth = (targetHeight / aspectRatio).toInt()
        if (tempWidth <= targetWidth) {
            finalHeight = targetHeight
            finalWidth = tempWidth
        } else {
            finalWidth = targetWidth
            finalHeight = (finalWidth * aspectRatio).toInt()
        }
    } else {
        val tempWidth = (targetHeight * aspectRatio).toInt()
        if (tempWidth <= targetWidth) {
            finalHeight = targetHeight
            finalWidth = tempWidth
        } else {
            finalWidth = targetWidth
            finalHeight = (finalWidth / aspectRatio).toInt()
        }
    }
    return Size(finalWidth, finalHeight)
}

/**
 * Calculates the aspect ratio of an activity from its fullscreen bounds.
 */
private fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float {
    if (taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed) {
        val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxWidth
        val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxHeight
        return maxOf(appLetterboxWidth, appLetterboxHeight) /
                minOf(appLetterboxWidth, appLetterboxHeight).toFloat()
    }
    val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f
    return maxOf(appBounds.height(), appBounds.width()) /
                minOf(appBounds.height(), appBounds.width()).toFloat()
}

/**
 * Calculates the desired initial bounds for applications in desktop windowing. This is done as a
 * scale of the screen bounds.
 */
private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size {
    val width = (screenBounds.width() * scale).toInt()
    val height = (screenBounds.height() * scale).toInt()
    return Size(width, height)
}

/**
 * Adjusts bounds to be positioned in the middle of the screen.
 */
private fun positionInScreen(desiredSize: Size, screenBounds: Rect): Rect {
    // TODO(b/325240051): Position apps with bottom heavy offset
    val heightOffset = (screenBounds.height() - desiredSize.height) / 2
    val widthOffset = (screenBounds.width() - desiredSize.width) / 2
    return Rect(widthOffset, heightOffset,
        desiredSize.width + widthOffset, desiredSize.height + heightOffset)
}
+24 −6
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.window.flags.Flags
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
@@ -85,7 +86,6 @@ import com.android.wm.shell.util.KtProtoLog
import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
import com.android.wm.shell.windowdecor.extension.isFreeform
import com.android.wm.shell.windowdecor.extension.isFullscreen
import java.io.PrintWriter
import java.util.Optional
@@ -203,6 +203,11 @@ class DesktopTasksController(
        dragAndDropController.addListener(this)
    }

    @VisibleForTesting
    fun getVisualIndicator(): DesktopModeVisualIndicator? {
        return visualIndicator
    }

    fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
        toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
        enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
@@ -605,8 +610,9 @@ class DesktopTasksController(
    }

    /**
     * Quick-resizes a desktop task, toggling between the stable bounds and the last saved bounds
     * if available or the default bounds otherwise.
     * Quick-resizes a desktop task, toggling between a fullscreen state (represented by the
     * stable bounds) and a free floating state (either the last saved bounds if available or the
     * default bounds otherwise).
     */
    fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo) {
        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
@@ -622,9 +628,13 @@ class DesktopTasksController(
                    desktopModeTaskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
            if (taskBoundsBeforeMaximize != null) {
                destinationBounds.set(taskBoundsBeforeMaximize)
            } else {
                if (Flags.enableWindowingDynamicInitialBounds()){
                    destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo))
                } else {
                    destinationBounds.set(getDefaultDesktopTaskBounds(displayLayout))
                }
            }
        } else {
            // Save current bounds so that task can be restored back to original bounds if necessary
            // and toggle to the stable bounds.
@@ -1011,6 +1021,7 @@ class DesktopTasksController(
        wct: WindowContainerTransaction,
        taskInfo: RunningTaskInfo
    ) {
        val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!!
        val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
        val targetWindowingMode = if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
@@ -1019,6 +1030,9 @@ class DesktopTasksController(
        } else {
            WINDOWING_MODE_FREEFORM
        }
        if (Flags.enableWindowingDynamicInitialBounds()) {
            wct.setBounds(taskInfo.token, calculateInitialBounds(displayLayout, taskInfo))
        }
        wct.setWindowingMode(taskInfo.token, targetWindowingMode)
        wct.reorder(taskInfo.token, true /* onTop */)
        if (isDesktopDensityOverrideSet()) {
@@ -1239,14 +1253,18 @@ class DesktopTasksController(
     * @param y height of drag, to be checked against status bar height.
     */
    fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
        val indicator = visualIndicator ?: return
        val indicator = getVisualIndicator() ?: return
        val indicatorType = indicator
            .updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
        when (indicatorType) {
            DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR -> {
                val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
                if (Flags.enableWindowingDynamicInitialBounds()) {
                    finalizeDragToDesktop(taskInfo, calculateInitialBounds(displayLayout, taskInfo))
                } else {
                    finalizeDragToDesktop(taskInfo, getDefaultDesktopTaskBounds(displayLayout))
                }
            }
            DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR,
            DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR -> {
                cancelDragToDesktop(taskInfo)
+0 −4
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.wm.shell.windowdecor.extension

import android.app.TaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.view.WindowInsetsController.APPEARANCE_LIGHT_CAPTION_BARS
import android.view.WindowInsetsController.APPEARANCE_TRANSPARENT_CAPTION_BAR_BACKGROUND
@@ -36,6 +35,3 @@ val TaskInfo.isLightCaptionBarAppearance: Boolean

val TaskInfo.isFullscreen: Boolean
    get() = windowingMode == WINDOWING_MODE_FULLSCREEN

val TaskInfo.isFreeform: Boolean
    get() = windowingMode == WINDOWING_MODE_FREEFORM
Loading