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

Commit b6a3d537 authored by Eghosa Ewansiha-Vlachavas's avatar Eghosa Ewansiha-Vlachavas
Browse files

[1/n] Introduce dynamic bounds calculation to prevent letterboxing

When entering desktop windowing, applications should no longer be
letterboxed. This means scaling down apps in SCM for all unresizable
apps so intial bounds match scale of the fullscreen bounds. This removes
letterboxing while ensuring the apps layout is not disrupted.

For portrait resizable apps when the device is in landscape, the height
of the initial bounds will match the desired app height for desktopmode
while the width will remain from the apps fullscreen width.

For landscape resizable apps when the device is portrait, the fullscreen
height of the app will be preserved while the width will be set to a
custom value.

Fixes: 325250852
Bug: 319820230
Bug: 324378380
Test: atest WMShellUnitTests:DesktopTasksControllerTesti
Change-Id: I43b5cacecf1505a670ccf31b9483ac35edc50a7f
parent a3dc0933
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