• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 @file:JvmName("DesktopModeUtils")
18 
19 package com.android.wm.shell.desktopmode
20 
21 import android.app.ActivityManager.RunningTaskInfo
22 import android.app.TaskInfo
23 import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
24 import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
25 import android.content.pm.ActivityInfo.LAUNCH_MULTIPLE
26 import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE
27 import android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK
28 import android.content.pm.ActivityInfo.LAUNCH_SINGLE_TASK
29 import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
30 import android.content.pm.ActivityInfo.isFixedOrientationLandscape
31 import android.content.pm.ActivityInfo.isFixedOrientationPortrait
32 import android.content.res.Configuration.ORIENTATION_LANDSCAPE
33 import android.content.res.Configuration.ORIENTATION_PORTRAIT
34 import android.graphics.Rect
35 import android.os.SystemProperties
36 import android.util.Size
37 import android.window.DesktopModeFlags
38 import com.android.wm.shell.ShellTaskOrganizer
39 import com.android.wm.shell.common.DisplayController
40 import com.android.wm.shell.common.DisplayLayout
41 import kotlin.math.ceil
42 
43 val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float =
44     SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
45 
46 val DESKTOP_MODE_LANDSCAPE_APP_PADDING: Int =
47     SystemProperties.getInt("persist.wm.debug.desktop_mode_landscape_app_padding", 25)
48 
49 /** Calculates the initial bounds to enter desktop, centered on the display. */
calculateDefaultDesktopTaskBoundsnull50 fun calculateDefaultDesktopTaskBounds(displayLayout: DisplayLayout): Rect {
51     // TODO(b/319819547): Account for app constraints so apps do not become letterboxed
52     val desiredWidth = (displayLayout.width() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
53     val desiredHeight = (displayLayout.height() * DESKTOP_MODE_INITIAL_BOUNDS_SCALE).toInt()
54     val heightOffset = (displayLayout.height() - desiredHeight) / 2
55     val widthOffset = (displayLayout.width() - desiredWidth) / 2
56     return Rect(widthOffset, heightOffset, desiredWidth + widthOffset, desiredHeight + heightOffset)
57 }
58 
59 /**
60  * Calculates the initial bounds required for an application to fill a scale of the display bounds
61  * without any letterboxing. This is done by taking into account the applications fullscreen size,
62  * aspect ratio, orientation and resizability to calculate an area this is compatible with the
63  * applications previous configuration.
64  */
65 @JvmOverloads
calculateInitialBoundsnull66 fun calculateInitialBounds(
67     displayLayout: DisplayLayout,
68     taskInfo: RunningTaskInfo,
69     scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE,
70     captionInsets: Int = 0,
71     requestedScreenOrientation: Int? = null,
72 ): Rect {
73     val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height())
74     val appAspectRatio = calculateAspectRatio(taskInfo)
75     val idealSize = calculateIdealSize(screenBounds, scale)
76     // If no top activity exists, apps fullscreen bounds and aspect ratio cannot be calculated.
77     // Instead default to the desired initial bounds.
78     val stableBounds = Rect()
79     displayLayout.getStableBoundsForDesktopMode(stableBounds)
80     if (hasFullscreenOverride(taskInfo)) {
81         // If the activity has a fullscreen override applied, it should be treated as
82         // resizeable and match the device orientation. Thus the ideal size can be
83         // applied.
84         return positionInScreen(idealSize, stableBounds)
85     }
86     val topActivityInfo =
87         taskInfo.topActivityInfo ?: return positionInScreen(idealSize, stableBounds)
88     val screenOrientation = requestedScreenOrientation ?: topActivityInfo.screenOrientation
89 
90     val initialSize: Size =
91         when (taskInfo.configuration.orientation) {
92             ORIENTATION_LANDSCAPE -> {
93                 if (taskInfo.canChangeAspectRatio) {
94                     if (isFixedOrientationPortrait(screenOrientation)) {
95                         // For portrait resizeable activities, respect apps fullscreen width but
96                         // apply ideal size height.
97                         Size(
98                             taskInfo.appCompatTaskInfo.topActivityAppBounds.width(),
99                             idealSize.height,
100                         )
101                     } else {
102                         // For landscape resizeable activities, simply apply ideal size.
103                         idealSize
104                     }
105                 } else {
106                     // If activity is unresizeable, regardless of orientation, calculate maximum
107                     // size (within the ideal size) maintaining original aspect ratio.
108                     maximizeSizeGivenAspectRatio(
109                         taskInfo,
110                         idealSize,
111                         appAspectRatio,
112                         captionInsets,
113                         screenOrientation,
114                     )
115                 }
116             }
117             ORIENTATION_PORTRAIT -> {
118                 val customPortraitWidthForLandscapeApp =
119                     screenBounds.width() - (DESKTOP_MODE_LANDSCAPE_APP_PADDING * 2)
120                 if (taskInfo.canChangeAspectRatio) {
121                     if (isFixedOrientationLandscape(screenOrientation)) {
122                         // For landscape resizeable activities, respect apps fullscreen height and
123                         // apply custom app width.
124                         Size(
125                             customPortraitWidthForLandscapeApp,
126                             taskInfo.appCompatTaskInfo.topActivityAppBounds.height(),
127                         )
128                     } else {
129                         // For portrait resizeable activities, simply apply ideal size.
130                         idealSize
131                     }
132                 } else {
133                     if (isFixedOrientationLandscape(screenOrientation)) {
134                         // For landscape unresizeable activities, apply custom app width to ideal
135                         // size and calculate maximum size with this area while maintaining original
136                         // aspect ratio.
137                         maximizeSizeGivenAspectRatio(
138                             taskInfo,
139                             Size(customPortraitWidthForLandscapeApp, idealSize.height),
140                             appAspectRatio,
141                             captionInsets,
142                             screenOrientation,
143                         )
144                     } else {
145                         // For portrait unresizeable activities, calculate maximum size (within the
146                         // ideal size) maintaining original aspect ratio.
147                         maximizeSizeGivenAspectRatio(
148                             taskInfo,
149                             idealSize,
150                             appAspectRatio,
151                             captionInsets,
152                             screenOrientation,
153                         )
154                     }
155                 }
156             }
157             else -> {
158                 idealSize
159             }
160         }
161 
162     return positionInScreen(initialSize, stableBounds)
163 }
164 
165 /**
166  * Calculates the maximized bounds of a task given in the given [DisplayLayout], taking resizability
167  * into consideration.
168  */
calculateMaximizeBoundsnull169 fun calculateMaximizeBounds(displayLayout: DisplayLayout, taskInfo: RunningTaskInfo): Rect {
170     val stableBounds = Rect()
171     displayLayout.getStableBounds(stableBounds)
172     if (taskInfo.isResizeable) {
173         // if resizable then expand to entire stable bounds (full display minus insets)
174         return Rect(stableBounds)
175     } else {
176         // if non-resizable then calculate max bounds according to aspect ratio
177         val activityAspectRatio = calculateAspectRatio(taskInfo)
178         val captionInsets =
179             taskInfo.configuration.windowConfiguration.appBounds?.let {
180                 it.top - taskInfo.configuration.windowConfiguration.bounds.top
181             } ?: 0
182         val newSize =
183             maximizeSizeGivenAspectRatio(
184                 taskInfo,
185                 Size(stableBounds.width(), stableBounds.height()),
186                 activityAspectRatio,
187                 captionInsets,
188             )
189         return centerInArea(newSize, stableBounds, stableBounds.left, stableBounds.top)
190     }
191 }
192 
193 /**
194  * Calculates the largest size that can fit in a given area while maintaining a specific aspect
195  * ratio.
196  */
maximizeSizeGivenAspectRationull197 fun maximizeSizeGivenAspectRatio(
198     taskInfo: RunningTaskInfo,
199     targetArea: Size,
200     aspectRatio: Float,
201     captionInsets: Int = 0,
202     requestedScreenOrientation: Int? = null,
203 ): Size {
204     val targetHeight = targetArea.height - captionInsets
205     val targetWidth = targetArea.width
206     val finalHeight: Int
207     val finalWidth: Int
208     // Get orientation either through top activity or task's orientation
209     val screenOrientation =
210         requestedScreenOrientation ?: taskInfo.topActivityInfo?.screenOrientation
211     if (taskInfo.hasPortraitTopActivity(screenOrientation)) {
212         val tempWidth = ceil(targetHeight / aspectRatio).toInt()
213         if (tempWidth <= targetWidth) {
214             finalHeight = targetHeight
215             finalWidth = tempWidth
216         } else {
217             finalWidth = targetWidth
218             finalHeight = ceil(finalWidth * aspectRatio).toInt()
219         }
220     } else {
221         val tempWidth = ceil(targetHeight * aspectRatio).toInt()
222         if (tempWidth <= targetWidth) {
223             finalHeight = targetHeight
224             finalWidth = tempWidth
225         } else {
226             finalWidth = targetWidth
227             finalHeight = ceil(finalWidth / aspectRatio).toInt()
228         }
229     }
230     return Size(finalWidth, finalHeight + captionInsets)
231 }
232 
233 /** Calculates the aspect ratio of an activity from its fullscreen bounds. */
calculateAspectRationull234 fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float {
235     val appBounds =
236         if (taskInfo.appCompatTaskInfo.topActivityAppBounds.isEmpty) {
237             taskInfo.configuration.windowConfiguration.appBounds
238                 ?: taskInfo.configuration.windowConfiguration.bounds
239         } else {
240             taskInfo.appCompatTaskInfo.topActivityAppBounds
241         }
242     return maxOf(appBounds.height(), appBounds.width()) /
243         minOf(appBounds.height(), appBounds.width()).toFloat()
244 }
245 
246 /** Returns whether the task is maximized. */
isTaskMaximizednull247 fun isTaskMaximized(taskInfo: RunningTaskInfo, displayController: DisplayController): Boolean {
248     val displayLayout =
249         displayController.getDisplayLayout(taskInfo.displayId)
250             ?: error("Could not get display layout for display=${taskInfo.displayId}")
251     val stableBounds = Rect()
252     displayLayout.getStableBounds(stableBounds)
253     return isTaskMaximized(taskInfo, stableBounds)
254 }
255 
256 /** Returns whether the task is maximized. */
isTaskMaximizednull257 fun isTaskMaximized(taskInfo: RunningTaskInfo, stableBounds: Rect): Boolean {
258     val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds
259     return if (taskInfo.isResizeable) {
260         isTaskBoundsEqual(currentTaskBounds, stableBounds)
261     } else {
262         isTaskWidthOrHeightEqual(currentTaskBounds, stableBounds)
263     }
264 }
265 
266 /** Returns true if task's width or height is maximized else returns false. */
isTaskWidthOrHeightEqualnull267 fun isTaskWidthOrHeightEqual(taskBounds: Rect, stableBounds: Rect): Boolean {
268     return taskBounds.width() == stableBounds.width() ||
269         taskBounds.height() == stableBounds.height()
270 }
271 
272 /** Returns true if task bound is equal to stable bounds else returns false. */
isTaskBoundsEqualnull273 fun isTaskBoundsEqual(taskBounds: Rect, stableBounds: Rect): Boolean {
274     return taskBounds == stableBounds
275 }
276 
277 /**
278  * Returns the task bounds a launching task should inherit from an existing running instance.
279  * Returns null if there are no bounds to inherit.
280  */
getInheritedExistingTaskBoundsnull281 fun getInheritedExistingTaskBounds(
282     taskRepository: DesktopRepository,
283     shellTaskOrganizer: ShellTaskOrganizer,
284     task: RunningTaskInfo,
285     deskId: Int,
286 ): Rect? {
287     if (!DesktopModeFlags.INHERIT_TASK_BOUNDS_FOR_TRAMPOLINE_TASK_LAUNCHES.isTrue) return null
288     val activeTask = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId).firstOrNull()
289     if (activeTask == null) return null
290     val lastTask = shellTaskOrganizer.getRunningTaskInfo(activeTask)
291     val lastTaskTopActivity = lastTask?.topActivity
292     val currentTaskTopActivity = task.topActivity
293     val intentFlags = task.baseIntent.flags
294     val launchMode = task.topActivityInfo?.launchMode ?: LAUNCH_MULTIPLE
295     return when {
296         // No running task activity to inherit bounds from.
297         lastTaskTopActivity == null -> null
298         // No current top activity to set bounds for.
299         currentTaskTopActivity == null -> null
300         // Top task is not an instance of the launching activity, do not inherit its bounds.
301         lastTaskTopActivity.packageName != currentTaskTopActivity.packageName -> null
302         // Top task is an instance of launching activity. Activity will be launching in a new
303         // task with the existing task also being closed. Inherit existing task bounds to
304         // prevent new task jumping.
305         (isLaunchingNewSingleTask(launchMode) && isClosingExitingInstance(intentFlags)) ->
306             lastTask.configuration.windowConfiguration.bounds
307         else -> null
308     }
309 }
310 
311 /**
312  * Returns true if the launch mode will result in a single new task being created for the activity.
313  */
isLaunchingNewSingleTasknull314 private fun isLaunchingNewSingleTask(launchMode: Int) =
315     launchMode == LAUNCH_SINGLE_TASK ||
316         launchMode == LAUNCH_SINGLE_INSTANCE ||
317         launchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK
318 
319 /**
320  * Returns true if the intent will result in an existing task instance being closed if a new one
321  * appears.
322  */
323 private fun isClosingExitingInstance(intentFlags: Int) =
324     (intentFlags and FLAG_ACTIVITY_CLEAR_TASK) != 0 ||
325         (intentFlags and FLAG_ACTIVITY_MULTIPLE_TASK) == 0
326 
327 /**
328  * Calculates the desired initial bounds for applications in desktop windowing. This is done as a
329  * scale of the screen bounds.
330  */
331 private fun calculateIdealSize(screenBounds: Rect, scale: Float): Size {
332     val width = (screenBounds.width() * scale).toInt()
333     val height = (screenBounds.height() * scale).toInt()
334     return Size(width, height)
335 }
336 
337 /** Adjusts bounds to be positioned in the middle of the screen. */
positionInScreennull338 private fun positionInScreen(desiredSize: Size, stableBounds: Rect): Rect =
339     Rect(0, 0, desiredSize.width, desiredSize.height).apply {
340         val offset = DesktopTaskPosition.Center.getTopLeftCoordinates(stableBounds, this)
341         offsetTo(offset.x, offset.y)
342     }
343 
344 /**
345  * Whether the activity's aspect ratio can be changed or if it should be maintained as if it was
346  * unresizeable.
347  */
348 private val TaskInfo.canChangeAspectRatio: Boolean
349     get() = isResizeable && !appCompatTaskInfo.hasMinAspectRatioOverride()
350 
351 /**
352  * Adjusts bounds to be positioned in the middle of the area provided, not necessarily the entire
353  * screen, as area can be offset by left and top start.
354  */
centerInAreanull355 fun centerInArea(desiredSize: Size, areaBounds: Rect, leftStart: Int, topStart: Int): Rect {
356     val heightOffset = (areaBounds.height() - desiredSize.height) / 2
357     val widthOffset = (areaBounds.width() - desiredSize.width) / 2
358 
359     val newLeft = leftStart + widthOffset
360     val newTop = topStart + heightOffset
361     val newRight = newLeft + desiredSize.width
362     val newBottom = newTop + desiredSize.height
363 
364     return Rect(newLeft, newTop, newRight, newBottom)
365 }
366 
TaskInfonull367 private fun TaskInfo.hasPortraitTopActivity(screenOrientation: Int?): Boolean {
368     val topActivityScreenOrientation = screenOrientation ?: SCREEN_ORIENTATION_UNSPECIFIED
369     val appBounds = configuration.windowConfiguration.appBounds
370 
371     return when {
372         // First check if activity has portrait screen orientation
373         topActivityScreenOrientation != SCREEN_ORIENTATION_UNSPECIFIED -> {
374             isFixedOrientationPortrait(topActivityScreenOrientation)
375         }
376 
377         // Then check if the activity is portrait when letterboxed
378         appCompatTaskInfo.isTopActivityLetterboxed -> appCompatTaskInfo.isTopActivityPillarboxShaped
379 
380         // Then check if the activity is portrait
381         appBounds != null -> appBounds.height() > appBounds.width()
382 
383         // Otherwise just take the orientation of the task
384         else -> isFixedOrientationPortrait(configuration.orientation)
385     }
386 }
387 
hasFullscreenOverridenull388 private fun hasFullscreenOverride(taskInfo: RunningTaskInfo): Boolean {
389     return taskInfo.appCompatTaskInfo.isUserFullscreenOverrideEnabled ||
390         taskInfo.appCompatTaskInfo.isSystemFullscreenOverrideEnabled
391 }
392