Files
Lawnchair/wmshell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
T
2026-01-10 20:48:16 +07:00

5261 lines
223 KiB
Kotlin

/*
* 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.wm.shell.desktopmode
import android.annotation.UserIdInt
import android.app.ActivityManager
import android.app.ActivityManager.RecentTaskInfo
import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityOptions
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.AppOpsManager
import android.app.KeyguardManager
import android.app.PendingIntent
import android.app.TaskInfo
import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
import android.app.WindowConfiguration.WindowingMode
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.Region
import android.os.Binder
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.SystemProperties
import android.os.UserHandle
import android.os.UserManager
import android.util.Slog
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
import android.view.Display.INVALID_DISPLAY
import android.view.DragEvent
import android.view.MotionEvent
import android.view.SurfaceControl
import android.view.SurfaceControl.Transaction
import android.view.WindowManager
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.view.WindowManager.transitTypeToString
import android.widget.Toast
import android.window.DesktopExperienceFlags
import android.window.DesktopExperienceFlags.DesktopExperienceFlag
import android.window.DesktopExperienceFlags.ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY
import android.window.DesktopExperienceFlags.ENABLE_NON_DEFAULT_DISPLAY_SPLIT
import android.window.DesktopExperienceFlags.ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY
import android.window.DesktopModeFlags
import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER
import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY
import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
import android.window.RemoteTransition
import android.window.SplashScreen.SPLASH_SCREEN_STYLE_ICON
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.annotation.BinderThread
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.SystemBarUtils.getDesktopViewAppHeaderHeightPx
import com.android.internal.protolog.ProtoLog
import com.android.internal.util.LatencyTracker
import com.android.window.flags.Flags
import com.android.wm.shell.Flags.enableFlexibleSplit
import com.android.wm.shell.R
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.bubbles.BubbleController
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayLayout
import com.android.wm.shell.common.ExternalInterfaceBinder
import com.android.wm.shell.common.HomeIntentProvider
import com.android.wm.shell.common.MultiInstanceHelper
import com.android.wm.shell.common.MultiInstanceHelper.Companion.getComponent
import com.android.wm.shell.common.RemoteCallable
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.common.SingleInstanceRemoteListener
import com.android.wm.shell.common.SyncTransactionQueue
import com.android.wm.shell.common.UserProfileContexts
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState
import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
import com.android.wm.shell.desktopmode.DesktopRepository.DeskChangeListener
import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.Companion.DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
import com.android.wm.shell.desktopmode.desktopfirst.DesktopFirstListenerManager
import com.android.wm.shell.desktopmode.desktopfirst.isDisplayDesktopFirst
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler
import com.android.wm.shell.desktopmode.multidesks.DeskTransition
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
import com.android.wm.shell.desktopmode.multidesks.PreserveDisplayRequestHandler
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
import com.android.wm.shell.draganddrop.DragAndDropController
import com.android.wm.shell.freeform.FreeformTaskTransitionStarter
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.recents.RecentTasksController
import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransitionState
import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING
import com.android.wm.shell.shared.R as SharedR
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ExternalThread
import com.android.wm.shell.shared.annotations.ShellDesktopThread
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.shared.desktopmode.DesktopConfig
import com.android.wm.shell.shared.desktopmode.DesktopFirstListener
import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy
import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource
import com.android.wm.shell.shared.desktopmode.DesktopState
import com.android.wm.shell.shared.desktopmode.DesktopTaskToFrontReason
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
import com.android.wm.shell.splitscreen.SplitScreenController
import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE
import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.sysui.UserChangeListener
import com.android.wm.shell.transition.FocusTransitionObserver
import com.android.wm.shell.transition.OneShotRemoteHandler
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
import com.android.wm.shell.windowdecor.extension.isFullscreen
import com.android.wm.shell.windowdecor.extension.isMultiWindow
import com.android.wm.shell.windowdecor.extension.requestingImmersive
import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
import java.io.PrintWriter
import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import kotlin.coroutines.suspendCoroutine
import kotlin.jvm.optionals.getOrNull
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* A callback to be invoked when a transition is started via |Transitions.startTransition| with the
* transition binder token that it produces.
*
* Useful when multiple components are appending WCT operations to a single transition that is
* started outside of their control, and each of them wants to track the transition lifecycle
* independently by cross-referencing the transition token with future ready-transitions.
*/
typealias RunOnTransitStart = (IBinder) -> Unit
/** Handles moving tasks in and out of desktop */
class DesktopTasksController(
private val context: Context,
shellInit: ShellInit,
private val shellCommandHandler: ShellCommandHandler,
private val shellController: ShellController,
private val displayController: DisplayController,
private val shellTaskOrganizer: ShellTaskOrganizer,
private val syncQueue: SyncTransactionQueue,
private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer,
private val dragAndDropController: DragAndDropController,
private val transitions: Transitions,
private val keyguardManager: KeyguardManager,
private val returnToDragStartAnimator: ReturnToDragStartAnimator,
private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler,
private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler,
private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
private val desktopImmersiveController: DesktopImmersiveController,
private val userRepositories: DesktopUserRepositories,
desktopRepositoryInitializer: DesktopRepositoryInitializer,
private val recentsTransitionHandler: RecentsTransitionHandler,
private val multiInstanceHelper: MultiInstanceHelper,
@ShellMainThread private val mainExecutor: ShellExecutor,
@ShellMainThread private val mainScope: CoroutineScope,
@ShellDesktopThread private val desktopExecutor: ShellExecutor,
private val desktopTasksLimiter: Optional<DesktopTasksLimiter>,
private val recentTasksController: RecentTasksController?,
private val interactionJankMonitor: InteractionJankMonitor,
@ShellMainThread private val handler: Handler,
private val focusTransitionObserver: FocusTransitionObserver,
private val desktopModeEventLogger: DesktopModeEventLogger,
private val desktopModeUiEventLogger: DesktopModeUiEventLogger,
private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
private val bubbleController: Optional<BubbleController>,
private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
private val desksOrganizer: DesksOrganizer,
private val desksTransitionObserver: DesksTransitionObserver,
private val userProfileContexts: UserProfileContexts,
private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
private val moveToDisplayTransitionHandler: DesktopModeMoveToDisplayTransitionHandler,
private val homeIntentProvider: HomeIntentProvider,
private val desktopState: DesktopState,
private val desktopConfig: DesktopConfig,
private val visualIndicatorUpdateScheduler: VisualIndicatorUpdateScheduler,
private val desktopFirstListenerManager: Optional<DesktopFirstListenerManager>,
) :
RemoteCallable<DesktopTasksController>,
Transitions.TransitionHandler,
DragAndDropController.DragAndDropListener,
UserChangeListener {
private val desktopMode: DesktopModeImpl
private var taskRepository: DesktopRepository
private var visualIndicator: DesktopModeVisualIndicator? = null
private var userId: Int
private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler =
DesktopModeShellCommandHandler(this, focusTransitionObserver)
private val latencyTracker: LatencyTracker
private val mOnAnimationFinishedCallback = { releaseVisualIndicator() }
private lateinit var snapEventHandler: SnapEventHandler
private val dragToDesktopStateListener =
object : DragToDesktopStateListener {
override fun onCommitToDesktopAnimationStart() {
removeVisualIndicator()
}
override fun onCancelToDesktopAnimationEnd() {
removeVisualIndicator()
}
override fun onTransitionInterrupted() {
removeVisualIndicator()
}
private fun removeVisualIndicator() {
visualIndicator?.fadeOutIndicator { releaseVisualIndicator() }
}
}
@VisibleForTesting var taskbarDesktopTaskListener: TaskbarDesktopTaskListener? = null
@VisibleForTesting
var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener? = null
/** Task id of the task currently being dragged from fullscreen/split. */
val draggingTaskId
get() = dragToDesktopTransitionHandler.draggingTaskId
@RecentsTransitionState private var recentsTransitionState = TRANSITION_STATE_NOT_RUNNING
private lateinit var splitScreenController: SplitScreenController
lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter
// Launch cookie used to identify a drag and drop transition to fullscreen after it has begun.
// Used to prevent handleRequest from moving the new fullscreen task to freeform.
private var dragAndDropFullscreenCookie: Binder? = null
// A listener that is invoked after a desk has been remove from the system. */
var onDeskRemovedListener: OnDeskRemovedListener? = null
// A handler for requests to preserve a disconnected display to potentially restore later.
var preserveDisplayRequestHandler: PreserveDisplayRequestHandler? = null
private val toDesktopAnimationDurationMs =
context.resources.getInteger(SharedR.integer.to_desktop_animation_duration_ms)
init {
desktopMode = DesktopModeImpl()
if (desktopState.canEnterDesktopMode) {
shellInit.addInitCallback({ onInit() }, this)
}
userId = ActivityManager.getCurrentUser()
taskRepository = userRepositories.getProfile(userId)
latencyTracker = LatencyTracker.getInstance(context)
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desktopRepositoryInitializer.deskRecreationFactory =
DeskRecreationFactory { deskUserId, destinationDisplayId, _ ->
// TODO: b/393978539 - One of the recreated desks may need to be activated by
// default in desktop-first.
createDeskRootSuspending(displayId = destinationDisplayId, userId = deskUserId)
}
}
}
private fun onInit() {
logD("onInit")
shellCommandHandler.addDumpCallback(this::dump, this)
shellCommandHandler.addCommandCallback("desktopmode", desktopModeShellCommandHandler, this)
shellController.addExternalInterface(
IDesktopMode.DESCRIPTOR,
{ createExternalInterface() },
this,
)
shellController.addUserChangeListener(this)
// Update the current user id again because it might be updated between init and onInit().
updateCurrentUser(ActivityManager.getCurrentUser())
transitions.addHandler(this)
dragToDesktopTransitionHandler.dragToDesktopStateListener = dragToDesktopStateListener
recentsTransitionHandler.addTransitionStateListener(
object : RecentsTransitionStateListener {
override fun onTransitionStateChanged(@RecentsTransitionState state: Int) {
logV(
"Recents transition state changed: %s",
RecentsTransitionStateListener.stateToString(state),
)
recentsTransitionState = state
}
}
)
dragAndDropController.addListener(this)
}
@VisibleForTesting
fun getVisualIndicator(): DesktopModeVisualIndicator? {
return visualIndicator
}
fun setOnTaskResizeAnimationListener(listener: OnTaskResizeAnimationListener) {
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
desktopImmersiveController.onTaskResizeAnimationListener = listener
}
fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
returnToDragStartAnimator.setTaskRepositionAnimationListener(listener)
}
/** Setter needed to avoid cyclic dependency. */
fun setSplitScreenController(controller: SplitScreenController) {
splitScreenController = controller
dragToDesktopTransitionHandler.setSplitScreenController(controller)
}
/** Setter to handle snap events */
fun setSnapEventHandler(handler: SnapEventHandler) {
snapEventHandler = handler
desktopTasksLimiter.ifPresent { it.snapEventHandler = snapEventHandler }
}
/** Returns the transition type for the given remote transition. */
private fun transitionType(remoteTransition: RemoteTransition?): Int {
if (remoteTransition == null) {
logV("RemoteTransition is null")
return TRANSIT_NONE
}
return TRANSIT_TO_FRONT
}
/**
* Shows all tasks, that are part of the desktop, on top of launcher. Brings the task with id
* [taskIdToReorderToFront] to front if provided and is already on the default desk on the given
* display.
*/
@Deprecated("Use activateDesk() instead.", ReplaceWith("activateDesk()"))
fun showDesktopApps(
displayId: Int,
remoteTransition: RemoteTransition? = null,
taskIdToReorderToFront: Int? = null,
) {
logV("showDesktopApps")
activateDefaultDeskInDisplay(displayId, remoteTransition, taskIdToReorderToFront)
}
/** Returns whether the given display has an active desk. */
fun isAnyDeskActive(displayId: Int): Boolean = taskRepository.isAnyDeskActive(displayId)
/** Returns the id of the active desk in [displayId]. */
fun getActiveDeskId(displayId: Int): Int? = taskRepository.getActiveDeskId(displayId)
/**
* Moves focused task to desktop mode for given [displayId].
*
* TODO(b/405381458): use focusTransitionObserver to get the focused task on a certain display
*/
fun moveFocusedTaskToDesktop(displayId: Int, transitionSource: DesktopModeTransitionSource) {
val allFocusedTasks = getFocusedNonDesktopTasks(displayId)
when (allFocusedTasks.size) {
0 -> return
// Full screen case
1 -> {
if (
desktopModeCompatPolicy.shouldDisableDesktopEntryPoints(
allFocusedTasks.single()
)
) {
return
}
moveTaskToDefaultDeskAndActivate(
allFocusedTasks.single().taskId,
transitionSource = transitionSource,
)
}
// Split-screen case where there are two focused tasks, then we find the child
// task to move to desktop.
2 -> {
val focusedTask = getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1])
if (desktopModeCompatPolicy.shouldDisableDesktopEntryPoints(focusedTask)) {
return
}
moveTaskToDefaultDeskAndActivate(
focusedTask.taskId,
transitionSource = transitionSource,
)
}
else ->
logW(
"DesktopTasksController: Cannot enter desktop, expected less " +
"than 3 focused tasks but found %d",
allFocusedTasks.size,
)
}
}
/**
* Returns all focused tasks in full screen or split screen mode in [displayId] when it is not
* the home activity.
*/
private fun getFocusedNonDesktopTasks(displayId: Int): List<RunningTaskInfo> =
shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo ->
val focused = taskInfo.isFocused
val isNotDesktop =
if (DesktopExperienceFlags.EXCLUDE_DESK_ROOTS_FROM_DESKTOP_TASKS.isTrue) {
!taskRepository.isActiveTask(taskInfo.taskId)
} else {
taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN ||
taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW
}
val isHome = taskInfo.activityType == ACTIVITY_TYPE_HOME
return@filter focused && isNotDesktop && !isHome
}
/** Returns child task from two focused tasks in split screen mode. */
private fun getSplitFocusedTask(task1: RunningTaskInfo, task2: RunningTaskInfo) =
if (task1.taskId == task2.parentTaskId) task2 else task1
@Deprecated("Use isDisplayDesktopFirst() instead.", ReplaceWith("isDisplayDesktopFirst()"))
private fun forceEnterDesktop(displayId: Int): Boolean {
if (DesktopExperienceFlags.ENABLE_DESKTOP_FIRST_BASED_DEFAULT_TO_DESKTOP_BUGFIX.isTrue) {
return rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(displayId)
}
if (!desktopState.enterDesktopByDefaultOnFreeformDisplay) {
return false
}
// Secondary displays are always desktop-first
if (displayId != DEFAULT_DISPLAY) {
return true
}
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
// A non-organized display (e.g., non-trusted virtual displays used in CTS) doesn't have
// TDA.
if (tdaInfo == null) {
logW(
"forceEnterDesktop cannot find DisplayAreaInfo for displayId=%d. This could happen" +
" when the display is a non-trusted virtual display.",
displayId,
)
return false
}
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val isFreeformDisplay = tdaWindowingMode == WINDOWING_MODE_FREEFORM
return isFreeformDisplay
}
/** Called when the recents transition that started while in desktop is finishing. */
fun onRecentsInDesktopAnimationFinishing(
transition: IBinder,
finishWct: WindowContainerTransaction,
returnToApp: Boolean,
activeDeskIdOnRecentsStart: Int?,
) {
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
logV(
"onRecentsInDesktopAnimationFinishing returnToApp=%b activeDeskIdOnRecentsStart=%d",
returnToApp,
activeDeskIdOnRecentsStart,
)
if (returnToApp) {
// Returning to the same desk, notify the snap event handler of recents animation
// ending to the same desk.
snapEventHandler.onRecentsAnimationEndedToSameDesk()
return
}
if (
activeDeskIdOnRecentsStart == null ||
!taskRepository.isDeskActive(activeDeskIdOnRecentsStart)
) {
// No desk was active or it is already inactive.
return
}
// At this point the recents transition is either finishing to home, to another non-desktop
// task or to a different desk than the one that was active when recents started. For all
// of those the desk that was active needs to be deactivated.
val runOnTransitStart =
performDesktopExitCleanUp(
wct = finishWct,
deskId = activeDeskIdOnRecentsStart,
displayId = DEFAULT_DISPLAY,
willExitDesktop = true,
// No need to clean up the wallpaper / home when coming from a recents transition.
skipWallpaperAndHomeOrdering = true,
// This is a recents-finish, so taskbar animation on transit start does not apply.
skipUpdatingExitDesktopListener = true,
)
runOnTransitStart?.invoke(transition)
}
/** Returns whether a new desk can be created. */
fun canCreateDesks(repository: DesktopRepository = this.taskRepository): Boolean {
val deskLimit = desktopConfig.maxDeskLimit
return deskLimit == 0 || repository.getNumberOfDesks() < deskLimit
}
/**
* Adds a new desk to the given display for the given user and invokes [onResult] once the desk
* is created, but necessarily activated.
*/
fun createDesk(
displayId: Int,
userId: Int = this.userId,
enforceDeskLimit: Boolean = true,
activateDesk: Boolean = false,
onResult: ((Int) -> Unit) = {},
) {
logV(
"createDesk displayId=%d, userId=%d enforceDeskLimit=%b",
displayId,
userId,
enforceDeskLimit,
)
if (!desktopState.isDesktopModeSupportedOnDisplay(displayId)) {
// Display does not support desktops, no-op.
logW("createDesk displayId $displayId does not support desktops, ignoring request")
return
}
val repository = userRepositories.getProfile(userId)
if (enforceDeskLimit && !canCreateDesks(repository)) {
// At the limit, no-op.
logW("createDesk already at desk-limit, ignoring request")
return
}
createDeskRoot(displayId, userId) { deskId ->
if (deskId == null) {
logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId)
} else {
repository.addDesk(displayId = displayId, deskId = deskId)
onResult(deskId)
if (activateDesk) {
activateDesk(deskId)
}
}
}
}
@Deprecated("Use createDeskSuspending() instead.", ReplaceWith("createDeskSuspending()"))
private fun createDeskImmediate(displayId: Int, userId: Int = this.userId): Int? {
logV("createDeskImmediate displayId=%d, userId=%d", displayId, userId)
val repository = userRepositories.getProfile(userId)
val deskId = createDeskRootImmediate(displayId, userId)
if (deskId == null) {
logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId)
return null
}
repository.addDesk(displayId = displayId, deskId = deskId)
return deskId
}
private suspend fun createDeskSuspending(
displayId: Int,
userId: Int,
enforceDeskLimit: Boolean,
): Int = suspendCoroutine { cont ->
createDesk(displayId = displayId, userId = userId, enforceDeskLimit = enforceDeskLimit) {
deskId ->
cont.resumeWith(Result.success(deskId))
}
}
private fun createDeskRoot(
displayId: Int,
userId: Int = this.userId,
onResult: (Int?) -> Unit,
) {
if (displayId == Display.INVALID_DISPLAY) {
logW("createDesk attempt with invalid displayId", displayId)
onResult(null)
return
}
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// In single-desk, the desk reuses the display id.
logD("createDesk reusing displayId=%d for single-desk", displayId)
onResult(displayId)
return
}
if (
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue &&
UserManager.isHeadlessSystemUserMode() &&
UserHandle.USER_SYSTEM == userId
) {
logW("createDesk ignoring attempt for system user")
onResult(null)
return
}
desksOrganizer.createDesk(displayId, userId) { deskId ->
logD(
"createDesk obtained deskId=%d for displayId=%d and userId=%d",
deskId,
displayId,
userId,
)
onResult(deskId)
}
}
@Deprecated(
"Use createDeskRootSuspending() instead.",
ReplaceWith("createDeskRootSuspending()"),
)
private fun createDeskRootImmediate(displayId: Int, userId: Int): Int? {
if (displayId == Display.INVALID_DISPLAY) {
logW("createDeskRootImmediate attempt with invalid displayId", displayId)
return null
}
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// In single-desk, the desk reuses the display id.
logD("createDeskRootImmediate reusing displayId=%d for single-desk", displayId)
return displayId
}
if (
DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue &&
UserManager.isHeadlessSystemUserMode() &&
UserHandle.USER_SYSTEM == userId
) {
logW("createDeskRootImmediate ignoring attempt for system user")
return null
}
return desksOrganizer.createDeskImmediate(displayId, userId)
}
private suspend fun createDeskRootSuspending(displayId: Int, userId: Int = this.userId): Int? =
suspendCoroutine { cont ->
createDeskRoot(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) }
}
private fun onDisplayDisconnect(
disconnectedDisplayId: Int,
destinationDisplayId: Int,
transition: IBinder,
): WindowContainerTransaction {
preserveDisplayRequestHandler?.requestPreserveDisplay(disconnectedDisplayId)
// TODO: b/406320371 - Verify this works with non-system users once the underlying bug is
// resolved.
val wct = WindowContainerTransaction()
// TODO: b/391652399 - Investigate why sometimes disconnect results in a black background.
// Additionally, investigate why wallpaper goes to front for inactive users.
val desktopModeSupportedOnDisplay =
desktopState.isDesktopModeSupportedOnDisplay(destinationDisplayId)
snapEventHandler.onDisplayDisconnected(disconnectedDisplayId, desktopModeSupportedOnDisplay)
removeWallpaperTask(wct, disconnectedDisplayId)
removeHomeTask(wct, disconnectedDisplayId)
userRepositories.forAllRepositories { desktopRepository ->
val deskIds = desktopRepository.getDeskIds(disconnectedDisplayId).toList()
if (desktopModeSupportedOnDisplay) {
// Desktop supported on display; reparent desks, focused desk on top.
for (deskId in deskIds) {
val deskTasks = desktopRepository.getActiveTaskIdsInDesk(deskId)
// Remove desk if it's empty.
if (deskTasks.isEmpty()) {
desksOrganizer.removeDesk(wct, deskId, desktopRepository.userId)
desksTransitionObserver.addPendingTransition(
DeskTransition.RemoveDesk(
token = transition,
displayId = disconnectedDisplayId,
deskId = deskId,
tasks = emptySet(),
onDeskRemovedListener = onDeskRemovedListener,
runOnTransitEnd = { snapEventHandler.onDeskRemoved(deskId) },
)
)
} else {
// Otherwise, reparent it to the destination display.
val toTop =
deskTasks.contains(focusTransitionObserver.globallyFocusedTaskId)
desksOrganizer.moveDeskToDisplay(wct, deskId, destinationDisplayId, toTop)
desksTransitionObserver.addPendingTransition(
DeskTransition.ChangeDeskDisplay(
transition,
deskId,
destinationDisplayId,
)
)
updateDesksActivationOnDisconnection(
deskId,
destinationDisplayId,
wct,
toTop,
)
?.invoke(transition)
}
}
} else {
// Desktop not supported on display; reparent tasks to display area, remove desk.
val tdaInfo =
checkNotNull(
rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(destinationDisplayId)
) {
"Expected to find displayAreaInfo for displayId=$destinationDisplayId"
}
for (deskId in deskIds) {
val taskIds = desktopRepository.getActiveTaskIdsInDesk(deskId)
for (taskId in taskIds) {
val task = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: continue
wct.reparent(
task.token,
tdaInfo.token,
focusTransitionObserver.globallyFocusedTaskId == task.taskId,
)
}
desksOrganizer.removeDesk(wct, deskId, userId)
desksTransitionObserver.addPendingTransition(
DeskTransition.RemoveDesk(
token = transition,
displayId = disconnectedDisplayId,
deskId = deskId,
tasks = emptySet(),
onDeskRemovedListener = onDeskRemovedListener,
runOnTransitEnd = { snapEventHandler.onDeskRemoved(deskId) },
)
)
desksTransitionObserver.addPendingTransition(
DeskTransition.RemoveDisplay(transition, disconnectedDisplayId)
)
}
}
}
return wct
}
/**
* Handle desk operations when disconnecting a display and all desks on that display are moving
* to a display that supports desks. The previously focused display will determine which desk
* will move to the front.
*
* @param disconnectedDisplayActiveDesk the id of the active desk on the disconnected display
* @param toTop whether this desk was reordered to the top
*/
@VisibleForTesting
fun updateDesksActivationOnDisconnection(
disconnectedDisplayActiveDesk: Int,
destinationDisplayId: Int,
wct: WindowContainerTransaction,
toTop: Boolean,
): RunOnTransitStart? {
val runOnTransitStart =
if (toTop) {
// The disconnected display's active desk was reparented to the top, activate it
// here.
addDeskActivationChanges(
deskId = disconnectedDisplayActiveDesk,
wct = wct,
displayId = destinationDisplayId,
)
} else {
// The disconnected display's active desk was reparented to the back, ensure it is
// no longer an active launch root.
prepareDeskDeactivationIfNeeded(wct, disconnectedDisplayActiveDesk)
}
return runOnTransitStart
}
private fun getDisplayIdForTaskOrDefault(task: TaskInfo?): Int {
// First, try to get the display already associated with the task.
if (
task != null &&
task.displayId != INVALID_DISPLAY &&
desktopState.isDesktopModeSupportedOnDisplay(displayId = task.displayId)
) {
return task.displayId
}
// Second, try to use the globally focused display.
val globallyFocusedDisplayId = focusTransitionObserver.globallyFocusedDisplayId
if (
globallyFocusedDisplayId != INVALID_DISPLAY &&
desktopState.isDesktopModeSupportedOnDisplay(displayId = globallyFocusedDisplayId)
) {
return globallyFocusedDisplayId
}
// Fallback to any display that supports desktop.
val supportedDisplayId =
rootTaskDisplayAreaOrganizer.displayIds.firstOrNull { displayId ->
desktopState.isDesktopModeSupportedOnDisplay(displayId)
}
if (supportedDisplayId != null) {
return supportedDisplayId
}
// Use the default display as the last option even if it does not support desktop. Callers
// should handle this case.
return DEFAULT_DISPLAY
}
/** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@JvmOverloads
fun moveTaskToDefaultDeskAndActivate(
taskId: Int,
wct: WindowContainerTransaction = WindowContainerTransaction(),
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
): Boolean {
val task =
shellTaskOrganizer.getRunningTaskInfo(taskId)
?: recentTasksController?.findTaskInBackground(taskId)
if (task == null) {
logW("moveTaskToDefaultDeskAndActivate taskId=%d not found", taskId)
return false
}
val displayId = getDisplayIdForTaskOrDefault(task)
if (
DesktopExperienceFlags.ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE.isTrue &&
!desktopState.isDesktopModeSupportedOnDisplay(displayId) &&
transitionSource != DesktopModeTransitionSource.ADB_COMMAND &&
transitionSource != DesktopModeTransitionSource.APP_FROM_OVERVIEW
) {
logW("moveTaskToDefaultDeskAndActivate display=$displayId does not support desk")
return false
}
if (
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
!DesktopExperienceFlags.ENABLE_DEFAULT_DESK_WITHOUT_WARMUP_MIGRATION.isTrue
) {
val deskId = getOrCreateDefaultDeskId(displayId) ?: return false
return moveTaskToDesk(
taskId = taskId,
deskId = deskId,
wct = wct,
transitionSource = transitionSource,
remoteTransition = remoteTransition,
)
}
mainScope.launch {
try {
moveTaskToDesk(
taskId = taskId,
deskId = getOrCreateDefaultDeskIdSuspending(displayId),
wct = wct,
transitionSource = transitionSource,
remoteTransition = remoteTransition,
)
} catch (t: Throwable) {
logE("Failed to move task to default desk: %s", t.message)
}
}
return true
}
/** Moves task to desktop mode if task is running, else launches it in desktop mode. */
fun moveTaskToDesk(
taskId: Int,
deskId: Int,
wct: WindowContainerTransaction = WindowContainerTransaction(),
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
): Boolean {
logV("moveTaskToDesk taskId=%d deskId=%d source=%s", taskId, deskId, transitionSource)
val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId)
if (runningTask != null) {
return moveRunningTaskToDesk(
task = runningTask,
deskId = deskId,
wct = wct,
transitionSource = transitionSource,
remoteTransition = remoteTransition,
callback = callback,
)
}
val backgroundTask = recentTasksController?.findTaskInBackground(taskId)
if (backgroundTask != null) {
return moveBackgroundTaskToDesktop(
taskId,
deskId,
wct,
transitionSource,
remoteTransition,
callback,
)
}
logW("moveTaskToDesk taskId=%d not found", taskId)
return false
}
private fun moveBackgroundTaskToDesktop(
taskId: Int,
deskId: Int,
wct: WindowContainerTransaction,
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
): Boolean {
val task = recentTasksController?.findTaskInBackground(taskId)
if (task == null) {
logW("moveBackgroundTaskToDesktop taskId=%d not found", taskId)
return false
}
logV("moveBackgroundTaskToDesktop with taskId=%d to deskId=%d", taskId, deskId)
val runOnTransitStart = addDeskActivationChanges(deskId, wct, task)
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
wct = wct,
displayId = DEFAULT_DISPLAY,
excludeTaskId = taskId,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
wct.startTask(
taskId,
ActivityOptions.makeBasic()
.apply { launchWindowingMode = WINDOWING_MODE_FREEFORM }
.toBundle(),
)
val transition: IBinder
if (remoteTransition != null) {
val transitionType = transitionType(remoteTransition)
val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition)
transition = transitions.startTransition(transitionType, wct, remoteTransitionHandler)
remoteTransitionHandler.setTransition(transition)
} else {
// TODO(343149901): Add DPI changes for task launch
transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
invokeCallbackToOverview(transition, callback)
}
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
if (!desktopState.enableMultipleDesktops) {
desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
toDesktopAnimationDurationMs
)
}
runOnTransitStart?.invoke(transition)
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
return true
}
/** Moves a running task to desktop. */
private fun moveRunningTaskToDesk(
task: RunningTaskInfo,
deskId: Int,
wct: WindowContainerTransaction = WindowContainerTransaction(),
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
callback: IMoveToDesktopCallback? = null,
): Boolean {
val displayId = taskRepository.getDisplayForDesk(deskId)
logV(
"moveRunningTaskToDesk taskId=%d deskId=%d displayId=%d",
task.taskId,
deskId,
displayId,
)
exitSplitIfApplicable(wct, task)
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
wct = wct,
displayId = displayId,
excludeTaskId = task.taskId,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
val runOnTransitStart = addDeskActivationWithMovingTaskChanges(deskId, wct, task)
val transition: IBinder
if (remoteTransition != null) {
val transitionType = transitionType(remoteTransition)
val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition)
transition = transitions.startTransition(transitionType, wct, remoteTransitionHandler)
remoteTransitionHandler.setTransition(transition)
} else {
transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource)
invokeCallbackToOverview(transition, callback)
}
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
if (!desktopState.enableMultipleDesktops) {
desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
toDesktopAnimationDurationMs
)
}
runOnTransitStart?.invoke(transition)
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
taskRepository.setActiveDesk(displayId = displayId, deskId = deskId)
}
return true
}
private fun invokeCallbackToOverview(transition: IBinder, callback: IMoveToDesktopCallback?) {
// TODO: b/333524374 - Remove this later.
// This is a temporary implementation for adding CUJ end and
// should be removed when animation is moved to launcher through remote transition.
if (callback != null) {
overviewToDesktopTransitionObserver.addPendingOverviewTransition(transition, callback)
}
}
/**
* The first part of the animated drag to desktop transition. This is followed with a call to
* [finalizeDragToDesktop] or [cancelDragToDesktop].
*/
fun startDragToDesktop(
taskInfo: RunningTaskInfo,
dragToDesktopValueAnimator: MoveToDesktopAnimator,
taskSurface: SurfaceControl,
dragInterruptedCallback: Runnable,
) {
logV("startDragToDesktop taskId=%d", taskInfo.taskId)
val jankConfigBuilder =
InteractionJankMonitor.Configuration.Builder.withSurface(
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD,
context,
taskSurface,
handler,
)
.setTimeout(APP_HANDLE_DRAG_HOLD_CUJ_TIMEOUT_MS)
interactionJankMonitor.begin(jankConfigBuilder)
dragToDesktopTransitionHandler.startDragToDesktopTransition(
taskInfo,
dragToDesktopValueAnimator,
visualIndicator,
dragInterruptedCallback,
)
}
/**
* The second part of the animated drag to desktop transition, called after
* [startDragToDesktop].
*/
private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) {
val deskId = getOrCreateDefaultDeskId(taskInfo.displayId) ?: return
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"DesktopTasksController: finalizeDragToDesktop taskId=%d deskId=%d",
taskInfo.taskId,
deskId,
)
val wct = WindowContainerTransaction()
exitSplitIfApplicable(wct, taskInfo)
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so
// this shouldn't be necessary at all.
if (ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue) {
moveHomeTaskToTop(taskInfo.displayId, wct)
} else {
moveHomeTaskToTop(context.displayId, wct)
}
}
val runOnTransitStart = addDeskActivationWithMovingTaskChanges(deskId, wct, taskInfo)
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
wct = wct,
displayId = taskInfo.displayId,
excludeTaskId = null,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
if (!desktopState.enableMultipleDesktops) {
desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS.toInt()
)
}
if (transition != null) {
runOnTransitStart?.invoke(transition)
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
taskRepository.setActiveDesk(displayId = taskInfo.displayId, deskId = deskId)
}
} else {
latencyTracker.onActionCancel(LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG)
}
}
/**
* Perform needed cleanup transaction once animation is complete. Bounds need to be set here
* instead of initial wct to both avoid flicker and to have task bounds to use for the staging
* animation.
*
* @param taskInfo task entering split that requires a bounds update
*/
fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
val wct = WindowContainerTransaction()
wct.setBounds(taskInfo.token, Rect())
if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue) {
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED)
}
shellTaskOrganizer.applyTransaction(wct)
}
/**
* Perform clean up of the desktop wallpaper activity if the closed window task is the last
* active task.
*
* @param wct transaction to modify if the last active task is closed
* @param displayId display id of the window that's being closed
* @param taskId task id of the window that's being closed
*/
fun onDesktopWindowClose(
wct: WindowContainerTransaction,
displayId: Int,
taskInfo: RunningTaskInfo,
): ((IBinder) -> Unit) {
val taskId = taskInfo.taskId
val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
error("Did not find desk for task: $taskId")
}
snapEventHandler.removeTaskIfTiled(displayId, taskId)
val shouldExitDesktop =
willExitDesktop(
triggerTaskId = taskInfo.taskId,
displayId = displayId,
forceExitDesktop = false,
)
val desktopExitRunnable =
performDesktopExitCleanUp(
wct = wct,
deskId = deskId,
displayId = displayId,
willExitDesktop = shouldExitDesktop,
shouldEndUpAtHome = true,
)
taskRepository.addClosingTask(displayId = displayId, deskId = deskId, taskId = taskId)
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId, taskId)
)
val immersiveRunnable =
desktopImmersiveController
.exitImmersiveIfApplicable(
wct = wct,
taskInfo = taskInfo,
reason = DesktopImmersiveController.ExitReason.CLOSED,
)
.asExit()
?.runOnTransitionStart
return { transitionToken ->
immersiveRunnable?.invoke(transitionToken)
desktopExitRunnable?.invoke(transitionToken)
}
}
/**
* Returns the task that will be focused next after the current task (the given [taskInfo]) is
* removed, due to being minimized or closed.
*
* @param taskInfo the task that is being removed.
* @return the taskId of the next focused task, or [INVALID_TASK_ID] if no task is found.
*/
fun getNextFocusedTask(taskInfo: RunningTaskInfo): Int {
val deskId = getOrCreateDefaultDeskId(taskInfo.displayId) ?: return INVALID_TASK_ID
return taskRepository
.getExpandedTasksIdsInDeskOrdered(deskId)
// exclude current task since maximize/restore transition has not taken place yet.
.filterNot { it == taskInfo.taskId }
.firstOrNull { !taskRepository.isClosingTask(it) } ?: INVALID_TASK_ID
}
fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
val wct = WindowContainerTransaction()
val taskId = taskInfo.taskId
val displayId = taskInfo.displayId
val deskId =
taskRepository.getDeskIdForTask(taskInfo.taskId)
?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
logW("minimizeTask: desk not found for task: ${taskInfo.taskId}")
return
} else {
getOrCreateDefaultDeskId(taskInfo.displayId)
}
val isLastTask =
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
taskRepository.isOnlyVisibleNonClosingTaskInDesk(
taskId = taskId,
deskId = checkNotNull(deskId) { "Expected non-null deskId" },
displayId = displayId,
)
} else {
taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
}
snapEventHandler.removeTaskIfTiled(displayId, taskId)
val isMinimizingToPip =
DesktopExperienceFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
(taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false) &&
isPipAllowedInAppOps(taskInfo)
logD(
"minimizeTask isMinimizingToPip=%b isAutoEnterEnabled=%b isPipAllowedInAppOps=%b",
isMinimizingToPip,
(taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false),
isPipAllowedInAppOps(taskInfo),
)
// If task is going to PiP, start a PiP transition instead of a minimize transition
if (isMinimizingToPip) {
val requestInfo =
TransitionRequestInfo(
TRANSIT_PIP,
/* triggerTask= */ null,
taskInfo,
/* remoteTransition= */ null,
/* displayChange= */ null,
/* flags= */ 0,
)
val requestRes =
transitions.dispatchRequest(SYNTHETIC_TRANSITION, requestInfo, /* skip= */ null)
wct.merge(requestRes.second, true)
// In multi-activity case, we either explicitly minimize the parent task, or reorder the
// parent task to the back so that it is not brought to the front and shown when the
// child task breaks off into PiP.
val isMultiActivityPip = taskInfo.numActivities > 1
if (isMultiActivityPip) {
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.minimizeTask(
wct = wct,
deskId = checkNotNull(deskId) { "Expected non-null deskId" },
task = taskInfo,
)
} else {
wct.reorder(taskInfo.token, /* onTop= */ false)
}
}
// If the task minimizing to PiP is the last task, modify wct to perform Desktop cleanup
var desktopExitRunnable: RunOnTransitStart? = null
if (isLastTask) {
desktopExitRunnable =
performDesktopExitCleanUp(
wct = wct,
deskId = deskId,
displayId = displayId,
willExitDesktop = true,
)
}
val transition = freeformTaskTransitionStarter.startPipTransition(wct)
if (isMultiActivityPip) {
desktopTasksLimiter.ifPresent {
it.addPendingMinimizeChange(
transition = transition,
displayId = displayId,
taskId = taskId,
minimizeReason = minimizeReason,
)
}
}
desktopExitRunnable?.invoke(transition)
} else {
val willExitDesktop =
if (
DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
DesktopExperienceFlags.ENABLE_EMPTY_DESK_ON_MINIMIZE.isTrue
)
false
else willExitDesktop(taskId, displayId, forceExitDesktop = false)
val desktopExitRunnable =
performDesktopExitCleanUp(
wct = wct,
deskId = deskId,
displayId = displayId,
willExitDesktop = willExitDesktop,
)
// Notify immersive handler as it might need to exit immersive state.
val exitResult =
desktopImmersiveController.exitImmersiveIfApplicable(
wct = wct,
taskInfo = taskInfo,
reason = DesktopImmersiveController.ExitReason.MINIMIZED,
)
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.minimizeTask(
wct = wct,
deskId = checkNotNull(deskId) { "Expected non-null deskId" },
task = taskInfo,
)
} else {
wct.reorder(taskInfo.token, /* onTop= */ false)
}
val transition =
freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
desktopTasksLimiter.ifPresent {
it.addPendingMinimizeChange(
transition = transition,
displayId = displayId,
taskId = taskId,
minimizeReason = minimizeReason,
)
}
exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
desktopExitRunnable?.invoke(transition)
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId, taskId)
)
}
/** Checks whether the given [taskInfo] is allowed to enter PiP in AppOps. */
private fun isPipAllowedInAppOps(taskInfo: RunningTaskInfo): Boolean {
val packageName =
taskInfo.baseActivity?.packageName
?: taskInfo.topActivity?.packageName
?: taskInfo.origActivity?.packageName
?: taskInfo.realActivity?.packageName
?: return false
val appOpsManager =
checkNotNull(context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager)
try {
val appInfo =
context.packageManager.getApplicationInfoAsUser(packageName, /* flags= */ 0, userId)
return appOpsManager.checkOpNoThrow(
AppOpsManager.OP_PICTURE_IN_PICTURE,
appInfo.uid,
packageName,
) == AppOpsManager.MODE_ALLOWED
} catch (_: PackageManager.NameNotFoundException) {
logW(
"isPipAllowedInAppOps: Failed to find applicationInfo for packageName=%s " +
"and userId=%d",
packageName,
userId,
)
}
return false
}
/** Move or launch a task with given [taskId] to fullscreen */
@JvmOverloads
fun moveToFullscreen(
taskId: Int,
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
) {
val taskInfo: TaskInfo? =
shellTaskOrganizer.getRunningTaskInfo(taskId)
?: if (enableAltTabKqsFlatenning.isTrue) {
recentTasksController?.findTaskInBackground(taskId)
} else {
null
}
taskInfo?.let { task ->
snapEventHandler.removeTaskIfTiled(task.displayId, taskId)
moveToFullscreenWithAnimation(
task,
task.positionInParent,
transitionSource,
remoteTransition,
)
}
}
/** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */
fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) {
getFocusedDesktopTask(displayId)?.let {
snapEventHandler.removeTaskIfTiled(displayId, it.taskId)
moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource)
}
}
private fun exitSplitIfApplicable(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
if (splitScreenController.isTaskInSplitScreen(taskInfo.taskId)) {
splitScreenController.prepareExitSplitScreen(
wct,
splitScreenController.getStageOfTask(taskInfo.taskId),
EXIT_REASON_DESKTOP_MODE,
)
splitScreenController.transitionHandler?.onSplitToDesktop()
}
}
/**
* The second part of the animated drag to desktop transition, called after
* [startDragToDesktop].
*/
fun cancelDragToDesktop(task: RunningTaskInfo) {
logV("cancelDragToDesktop taskId=%d", task.taskId)
dragToDesktopTransitionHandler.cancelDragToDesktopTransition(
DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
)
}
private fun moveToFullscreenWithAnimation(
task: TaskInfo,
position: Point,
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition? = null,
) {
logV("moveToFullscreenWithAnimation taskId=%d", task.taskId)
val displayId =
when {
task.displayId != INVALID_DISPLAY -> task.displayId
focusTransitionObserver.globallyFocusedDisplayId != INVALID_DISPLAY ->
focusTransitionObserver.globallyFocusedDisplayId
else -> DEFAULT_DISPLAY
}
val wct = WindowContainerTransaction()
// When a task is background, update wct to start task.
if (
enableAltTabKqsFlatenning.isTrue &&
shellTaskOrganizer.getRunningTaskInfo(task.taskId) == null &&
task is RecentTaskInfo
) {
wct.startTask(
task.taskId,
// TODO(b/400817258): Use ActivityOptions Utils when available.
ActivityOptions.makeBasic()
.apply {
launchWindowingMode = WINDOWING_MODE_FULLSCREEN
launchDisplayId = displayId
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
}
.toBundle(),
)
}
val willExitDesktop = willExitDesktop(task.taskId, displayId, forceExitDesktop = true)
val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop, displayId)
// We are moving a freeform task to fullscreen, put the home task under the fullscreen task.
if (!forceEnterDesktop(displayId)) {
moveHomeTaskToTop(displayId, wct)
wct.reorder(task.token, /* onTop= */ true)
}
val transition =
if (remoteTransition != null) {
val transitionType = transitionType(remoteTransition)
val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition)
transitions.startTransition(transitionType, wct, remoteTransitionHandler).also {
remoteTransitionHandler.setTransition(it)
}
} else {
exitDesktopTaskTransitionHandler.startTransition(
transitionSource,
wct,
position,
mOnAnimationFinishedCallback,
)
}
deactivationRunnable?.invoke(transition)
// handles case where we are moving to full screen without closing all DW tasks.
if (
!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)
// This callback is already invoked by |addMoveToFullscreenChanges| when this flag is
// enabled.
&&
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
&&
!desktopState.enableMultipleDesktops
) {
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION,
shouldEndUpAtHome = false,
)
}
}
/**
* Move a task to the front, using [remoteTransition].
*
* Note: beyond moving a task to the front, this method will minimize a task if we reach the
* Desktop task limit, so [remoteTransition] should also handle any such minimize change.
*/
@JvmOverloads
fun moveTaskToFront(
taskId: Int,
remoteTransition: RemoteTransition? = null,
unminimizeReason: UnminimizeReason,
) {
val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
if (task == null) {
moveBackgroundTaskToFront(taskId, remoteTransition, unminimizeReason)
} else {
moveTaskToFront(task, remoteTransition, unminimizeReason)
}
}
/**
* Launch a background task in desktop. Note that this should be used when we are already in
* desktop. If outside of desktop and want to launch a background task in desktop, use
* [moveBackgroundTaskToDesktop] instead.
*/
private fun moveBackgroundTaskToFront(
taskId: Int,
remoteTransition: RemoteTransition?,
unminimizeReason: UnminimizeReason,
) {
logV("moveBackgroundTaskToFront taskId=%s unminimizeReason=%s", taskId, unminimizeReason)
val wct = WindowContainerTransaction()
val deskIdForTask = taskRepository.getDeskIdForTask(taskId)
val deskId =
if (deskIdForTask != null) {
deskIdForTask
} else {
val task = recentTasksController?.findTaskInBackground(taskId)
val displayId = getDisplayIdForTaskOrDefault(task)
logV(
"background taskId=%s did not have desk associated, " +
"using default desk of displayId=%d",
taskId,
displayId,
)
getOrCreateDefaultDeskId(displayId) ?: return
}
val displayId =
if (ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue)
taskRepository.getDisplayForDesk(deskId)
else DEFAULT_DISPLAY
wct.startTask(
taskId,
ActivityOptions.makeBasic()
.apply {
launchWindowingMode = WINDOWING_MODE_FREEFORM
launchDisplayId = displayId
}
.toBundle(),
)
startLaunchTransition(
TRANSIT_OPEN,
wct,
taskId,
deskId = deskId,
displayId = displayId,
remoteTransition = remoteTransition,
unminimizeReason = unminimizeReason,
)
}
/**
* Move a task to the front, using [remoteTransition].
*
* Note: beyond moving a task to the front, this method will minimize a task if we reach the
* Desktop task limit, so [remoteTransition] should also handle any such minimize change.
*/
@JvmOverloads
fun moveTaskToFront(
taskInfo: RunningTaskInfo,
remoteTransition: RemoteTransition? = null,
unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN,
) {
val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
logV("moveTaskToFront taskId=%s deskId=%s", taskInfo.taskId, deskId)
val wct = WindowContainerTransaction()
addMoveTaskToFrontChanges(wct = wct, deskId = deskId, taskInfo = taskInfo)
startLaunchTransition(
transitionType = TRANSIT_TO_FRONT,
wct = wct,
launchingTaskId = taskInfo.taskId,
remoteTransition = remoteTransition,
deskId = deskId,
displayId = taskInfo.displayId,
unminimizeReason = unminimizeReason,
)
}
/** Applies the necessary [wct] changes to move [taskInfo] to front. */
fun addMoveTaskToFrontChanges(
wct: WindowContainerTransaction,
deskId: Int?,
taskInfo: RunningTaskInfo,
) {
logV("addMoveTaskToFrontChanges taskId=%s deskId=%s", taskInfo.taskId, deskId)
// If a task is tiled, another task should be brought to foreground with it so let
// tiling controller handle the request.
if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) {
return
}
if (deskId == null || !DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// Not a desktop task, just move to the front.
wct.reorder(taskInfo.token, /* onTop= */ true, /* includingParents= */ true)
} else {
// A desktop task with multiple desks enabled, reorder it within its desk.
desksOrganizer.reorderTaskToFront(wct, deskId, taskInfo)
}
}
/**
* Starts a launch transition with [transitionType] using [wct].
*
* @param transitionType the type of transition to start.
* @param wct the wct to use in the transition, which may already container changes.
* @param launchingTaskId the id of task launching, may be null if starting the task through an
* intent in the [wct].
* @param remoteTransition the remote transition associated with this transition start.
* @param deskId may be null if the launching task isn't launching into a desk, such as when
* fullscreen or split tasks are just moved to front.
* @param displayId the display in which the launch is happening.
* @param unminimizeReason the reason to unminimize.
*/
@VisibleForTesting
fun startLaunchTransition(
transitionType: Int,
wct: WindowContainerTransaction,
launchingTaskId: Int?,
remoteTransition: RemoteTransition? = null,
deskId: Int?,
displayId: Int,
unminimizeReason: UnminimizeReason = UnminimizeReason.UNKNOWN,
dragEvent: DragEvent? = null,
): IBinder {
logV(
"startLaunchTransition type=%s launchingTaskId=%d deskId=%d displayId=%d",
transitTypeToString(transitionType),
launchingTaskId,
deskId,
displayId,
)
// TODO: b/397619806 - Consolidate sharable logic with [handleFreeformTaskLaunch].
var launchTransaction = wct
// TODO: b/32994943 - remove dead code when cleaning up task_limit_separate_transition flag
val taskIdToMinimize =
deskId?.let {
addAndGetMinimizeChanges(
deskId = it,
wct = launchTransaction,
newTaskId = launchingTaskId,
launchingNewIntent = launchingTaskId == null,
)
}
val closingTopTransparentTaskId =
deskId?.let { taskRepository.getTopTransparentFullscreenTaskData(it)?.taskId }
val exitImmersiveResult =
desktopImmersiveController.exitImmersiveIfApplicable(
wct = launchTransaction,
displayId = displayId,
excludeTaskId = launchingTaskId,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
var activationRunOnTransitStart: RunOnTransitStart? = null
val shouldActivateDesk =
when {
deskId == null -> false
DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ->
!taskRepository.isDeskActive(deskId)
DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue ->
!isAnyDeskActive(displayId)
else -> false
}
if (shouldActivateDesk) {
val activateDeskWct = WindowContainerTransaction()
// TODO: b/391485148 - pass in the launching task here to apply task-limit policy,
// but make sure to not do it twice since it is also done at the start of this
// function.
activationRunOnTransitStart =
addDeskActivationChanges(
deskId = checkNotNull(deskId) { "Desk id must be non-null when activating" },
wct = activateDeskWct,
)
// Desk activation must be handled before app launch-related transactions.
activateDeskWct.merge(launchTransaction, /* transfer= */ true)
launchTransaction = activateDeskWct
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
if (!desktopState.enableMultipleDesktops) {
desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
toDesktopAnimationDurationMs
)
}
}
// Remove top transparent fullscreen task if needed.
deskId?.let { closeTopTransparentFullscreenTask(launchTransaction, it) }
val t =
if (remoteTransition == null) {
logV("startLaunchTransition -- no remoteTransition -- wct = $launchTransaction")
desktopMixedTransitionHandler.startLaunchTransition(
transitionType = transitionType,
wct = launchTransaction,
taskId = launchingTaskId,
minimizingTaskId = taskIdToMinimize,
closingTopTransparentTaskId = closingTopTransparentTaskId,
exitingImmersiveTask = exitImmersiveResult.asExit()?.exitingTask,
dragEvent = dragEvent,
)
} else if (taskIdToMinimize == null) {
// TODO(b/412761429): Move OneShotRemoteHandler call to within
// DesktopMixedTransitionHandler.
val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition)
transitions
.startTransition(transitionType, launchTransaction, remoteTransitionHandler)
.also { remoteTransitionHandler.setTransition(it) }
} else {
val remoteTransitionHandler =
DesktopWindowLimitRemoteHandler(
mainExecutor,
rootTaskDisplayAreaOrganizer,
remoteTransition,
taskIdToMinimize,
)
transitions
.startTransition(transitionType, launchTransaction, remoteTransitionHandler)
.also { remoteTransitionHandler.setTransition(it) }
}
if (taskIdToMinimize != null) {
addPendingMinimizeTransition(t, taskIdToMinimize, MinimizeReason.TASK_LIMIT)
}
if (deskId != null) {
addPendingTaskLimitTransition(t, deskId, launchingTaskId)
}
if (launchingTaskId != null && taskRepository.isMinimizedTask(launchingTaskId)) {
addPendingUnminimizeTransition(t, displayId, launchingTaskId, unminimizeReason)
}
activationRunOnTransitStart?.invoke(t)
exitImmersiveResult.asExit()?.runOnTransitionStart?.invoke(t)
return t
}
/**
* Move task to the next display.
*
* Queries all currently known display IDs and checks if they match the predicate. The check is
* performed in an order such that display IDs greater than the passed task's displayId are
* considered before display IDs less than or equal to the passed task's displayID. Within each
* of these two groups, the check is performed in ascending order.
*
* If a display ID matches predicate is found, re-parents the task to that display. No-op if no
* such display is found.
*/
fun moveToNextDisplay(taskId: Int, predicate: (Int) -> Boolean = { true }) {
val task = shellTaskOrganizer.getRunningTaskInfo(taskId)
if (task == null) {
logW("moveToNextDisplay: taskId=%d not found", taskId)
return
}
val newDisplayId =
rootTaskDisplayAreaOrganizer.displayIds
.sortedBy { (it - task.displayId - 1).mod(Int.MAX_VALUE) }
.find(predicate)
if (newDisplayId == null) {
logW("moveToNextDisplay: next display not found")
return
}
// moveToDisplay is no-op if newDisplayId is same with task.displayId.
moveToDisplay(task, newDisplayId)
}
/** Move task to the next display which can host desktop tasks. */
fun moveToNextDesktopDisplay(taskId: Int) =
moveToNextDisplay(taskId) { displayId ->
desktopState.isDesktopModeSupportedOnDisplay(displayId)
}
/**
* Start an intent through a launch transition for starting tasks whose transition does not get
* handled by [handleRequest]
*/
fun startLaunchIntentTransition(intent: Intent, options: Bundle, displayId: Int) {
val wct = WindowContainerTransaction()
val displayLayout = displayController.getDisplayLayout(displayId) ?: return
val bounds = calculateDefaultDesktopTaskBounds(displayLayout)
val deskId = getOrCreateDefaultDeskId(displayId) ?: return
if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue) {
val stableBounds = Rect().also { displayLayout.getStableBounds(it) }
cascadeWindow(bounds, displayLayout, deskId, stableBounds)
}
val pendingIntent =
PendingIntent.getActivityAsUser(
context,
/* requestCode= */ 0,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT,
/* options= */ null,
UserHandle.of(userId),
)
val ops =
ActivityOptions.fromBundle(options).apply {
launchWindowingMode = WINDOWING_MODE_FREEFORM
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
launchBounds = bounds
launchDisplayId = displayId
if (DesktopModeFlags.ENABLE_SHELL_INITIAL_BOUNDS_REGRESSION_BUG_FIX.isTrue) {
// Sets launch bounds size as flexible so core can recalculate.
flexibleLaunchSize = true
}
}
wct.sendPendingIntent(pendingIntent, intent, ops.toBundle())
startLaunchTransition(
TRANSIT_OPEN,
wct,
launchingTaskId = null,
deskId = deskId,
displayId = displayId,
)
}
/**
* Move [task] to display with [displayId]. When [bounds] is not null, it will be used as the
* bounds on the new display. When [transitionHandler] is not null, it will be used instead of
* the default [DesktopModeMoveToDisplayTransitionHandler].
*
* No-op if task is already on that display per [RunningTaskInfo.displayId].
*
* TODO: b/399411604 - split this up into smaller functions.
*/
private fun moveToDisplay(
task: RunningTaskInfo,
displayId: Int,
bounds: Rect? = null,
transitionHandler: TransitionHandler? = null,
) {
logV("moveToDisplay: taskId=%d displayId=%d", task.taskId, displayId)
if (task.displayId == displayId) {
logD("moveToDisplay: task already on display %d", displayId)
return
}
if (splitScreenController.isTaskInSplitScreen(task.taskId)) {
moveSplitPairToDisplay(task, displayId)
return
}
val wct = WindowContainerTransaction()
val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
if (displayAreaInfo == null) {
logW("moveToDisplay: display not found")
return
}
val destinationDeskId = taskRepository.getDefaultDeskId(displayId)
if (destinationDeskId == null) {
logW("moveToDisplay: desk not found for display: $displayId")
return
}
snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
// TODO: b/393977830 and b/397437641 - do not assume that freeform==desktop.
if (!task.isFreeform) {
addMoveToDeskTaskChanges(wct = wct, task = task, deskId = destinationDeskId)
} else {
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.moveTaskToDesk(wct, destinationDeskId, task)
}
if (bounds != null) {
wct.setBounds(task.token, bounds)
} else if (DesktopExperienceFlags.ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT.isTrue) {
applyFreeformDisplayChange(wct, task, displayId, destinationDeskId)
}
}
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true)
}
val activationRunnable = addDeskActivationChanges(destinationDeskId, wct, task)
val sourceDisplayId = task.displayId
val sourceDeskId = taskRepository.getDeskIdForTask(task.taskId)
val shouldExitDesktopIfNeeded =
ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue ||
DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
val deactivationRunnable =
if (shouldExitDesktopIfNeeded) {
performDesktopExitCleanupIfNeeded(
taskId = task.taskId,
deskId = sourceDeskId,
displayId = sourceDisplayId,
wct = wct,
forceToFullscreen = false,
)
} else {
null
}
if (DesktopExperienceFlags.ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS.isTrue) {
// Bring the destination display to top with includingParents=true, so that the
// destination display gains the display focus, which makes the top task in the display
// gains the global focus. This must be done after performDesktopExitCleanupIfNeeded.
// The method launches Launcher on the source display when the last task is moved, which
// brings the source display to the top. Calling reorder after
// performDesktopExitCleanupIfNeeded ensures that the destination display becomes the
// top (focused) display.
wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true)
}
val transition =
transitions.startTransition(
TRANSIT_CHANGE,
wct,
transitionHandler ?: moveToDisplayTransitionHandler,
)
deactivationRunnable?.invoke(transition)
activationRunnable?.invoke(transition)
}
/**
* Move split pair associated with the [task] to display with [displayId].
*
* No-op if task is already on that display per [RunningTaskInfo.displayId].
*/
private fun moveSplitPairToDisplay(task: RunningTaskInfo, displayId: Int) {
if (!splitScreenController.isTaskInSplitScreen(task.taskId)) {
return
}
if (
!ENABLE_NON_DEFAULT_DISPLAY_SPLIT.isTrue ||
!DesktopExperienceFlags.ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT.isTrue
) {
return
}
val wct = WindowContainerTransaction()
val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
if (displayAreaInfo == null) {
logW("moveSplitPairToDisplay: display not found")
return
}
val activeDeskId = taskRepository.getActiveDeskId(displayId)
logV("moveSplitPairToDisplay: moving split root to displayId=%d", displayId)
val stageCoordinatorRootTaskToken =
splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(DEFAULT_DISPLAY)
if (stageCoordinatorRootTaskToken == null) {
return
}
wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
val deactivationRunnable =
if (activeDeskId != null) {
// Split is being placed on top of an existing desk in the target display. Make
// sure it is cleaned up.
performDesktopExitCleanUp(
wct = wct,
deskId = activeDeskId,
displayId = displayId,
willExitDesktop = true,
shouldEndUpAtHome = false,
)
} else {
null
}
val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
deactivationRunnable?.invoke(transition)
return
}
/**
* 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, interaction: ToggleTaskSizeInteraction) {
val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds
desktopModeEventLogger.logTaskResizingStarted(
interaction.resizeTrigger,
interaction.inputMethod,
taskInfo,
currentTaskBounds.width(),
currentTaskBounds.height(),
displayController,
)
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
val destinationBounds = Rect()
val isMaximized = interaction.direction == ToggleTaskSizeInteraction.Direction.RESTORE
// If the task is currently maximized, we will toggle it not to be and vice versa. This is
// helpful to eliminate the current task from logic to calculate taskbar corner rounding.
val willMaximize = interaction.direction == ToggleTaskSizeInteraction.Direction.MAXIMIZE
if (isMaximized) {
// The desktop task is at the maximized width and/or height of the stable bounds.
// If the task's pre-maximize stable bounds were saved, toggle the task to those bounds.
// Otherwise, toggle to the default bounds.
val taskBoundsBeforeMaximize =
taskRepository.removeBoundsBeforeMaximize(taskInfo.taskId)
if (taskBoundsBeforeMaximize != null) {
destinationBounds.set(taskBoundsBeforeMaximize)
} else {
if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) {
destinationBounds.set(calculateInitialBounds(displayLayout, taskInfo))
} else {
destinationBounds.set(calculateDefaultDesktopTaskBounds(displayLayout))
}
}
} else {
// Save current bounds so that task can be restored back to original bounds if necessary
// and toggle to the stable bounds.
snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds)
destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo))
}
val shouldRestoreToSnap =
isMaximized && isTaskSnappedToHalfScreen(taskInfo, destinationBounds)
logD("willMaximize = %s", willMaximize)
logD("shouldRestoreToSnap = %s", shouldRestoreToSnap)
val doesAnyTaskRequireTaskbarRounding =
willMaximize ||
shouldRestoreToSnap ||
doesAnyTaskRequireTaskbarRounding(taskInfo.displayId, taskInfo.taskId)
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(doesAnyTaskRequireTaskbarRounding)
val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
interaction.uiEvent?.let { uiEvent -> desktopModeUiEventLogger.log(taskInfo, uiEvent) }
desktopModeEventLogger.logTaskResizingEnded(
interaction.resizeTrigger,
interaction.inputMethod,
taskInfo,
destinationBounds.width(),
destinationBounds.height(),
displayController,
)
toggleResizeDesktopTaskTransitionHandler.startTransition(
wct,
interaction.animationStartBounds,
)
}
private fun dragToMaximizeDesktopTask(
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl,
currentDragBounds: Rect,
motionEvent: MotionEvent,
) {
if (isTaskMaximized(taskInfo, displayController)) {
// Handle the case where we attempt to drag-to-maximize when already maximized: the task
// position won't need to change but we want to animate the surface going back to the
// maximized position.
val containerBounds = taskInfo.configuration.windowConfiguration.bounds
if (containerBounds != currentDragBounds) {
returnToDragStartAnimator.start(
taskInfo.taskId,
taskSurface,
startBounds = currentDragBounds,
endBounds = containerBounds,
)
}
return
}
toggleDesktopTaskSize(
taskInfo,
ToggleTaskSizeInteraction(
direction = ToggleTaskSizeInteraction.Direction.MAXIMIZE,
source = ToggleTaskSizeInteraction.Source.HEADER_DRAG_TO_TOP,
inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent),
animationStartBounds = currentDragBounds,
),
)
}
private fun isMaximizedToStableBoundsEdges(
taskInfo: RunningTaskInfo,
stableBounds: Rect,
): Boolean {
val currentTaskBounds = taskInfo.configuration.windowConfiguration.bounds
return isTaskBoundsEqual(currentTaskBounds, stableBounds)
}
/** Returns if current task bound is snapped to half screen */
private fun isTaskSnappedToHalfScreen(
taskInfo: RunningTaskInfo,
taskBounds: Rect = taskInfo.configuration.windowConfiguration.bounds,
): Boolean =
getSnapBounds(taskInfo.displayId, SnapPosition.LEFT) == taskBounds ||
getSnapBounds(taskInfo.displayId, SnapPosition.RIGHT) == taskBounds
@VisibleForTesting
fun doesAnyTaskRequireTaskbarRounding(displayId: Int, excludeTaskId: Int? = null): Boolean {
val doesAnyTaskRequireTaskbarRounding =
taskRepository
.getExpandedTasksOrdered(displayId)
// exclude current task since maximize/restore transition has not taken place yet.
.filterNot { taskId -> taskId == excludeTaskId }
.any { taskId ->
val taskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId) ?: return false
val displayLayout = displayController.getDisplayLayout(taskInfo.displayId)
val stableBounds = Rect().also { displayLayout?.getStableBounds(it) }
logD("taskInfo = %s", taskInfo)
logD(
"isTaskSnappedToHalfScreen(taskInfo) = %s",
isTaskSnappedToHalfScreen(taskInfo),
)
logD(
"isMaximizedToStableBoundsEdges(taskInfo, stableBounds) = %s",
isMaximizedToStableBoundsEdges(taskInfo, stableBounds),
)
isTaskSnappedToHalfScreen(taskInfo) ||
isMaximizedToStableBoundsEdges(taskInfo, stableBounds)
}
logD("doesAnyTaskRequireTaskbarRounding = %s", doesAnyTaskRequireTaskbarRounding)
return doesAnyTaskRequireTaskbarRounding
}
/**
* Quick-resize to the right or left half of the stable bounds.
*
* @param taskInfo current task that is being snap-resized via dragging or maximize menu button
* @param taskSurface the leash of the task being dragged
* @param currentDragBounds current position of the task leash being dragged (or current task
* bounds if being snapped resize via maximize menu button)
* @param position the portion of the screen (RIGHT or LEFT) we want to snap the task to.
*/
fun snapToHalfScreen(
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl?,
currentDragBounds: Rect,
position: SnapPosition,
resizeTrigger: ResizeTrigger,
inputMethod: InputMethod,
) {
desktopModeEventLogger.logTaskResizingStarted(
resizeTrigger,
inputMethod,
taskInfo,
currentDragBounds.width(),
currentDragBounds.height(),
displayController,
)
val destinationBounds = getSnapBounds(taskInfo.displayId, position)
desktopModeEventLogger.logTaskResizingEnded(
resizeTrigger,
inputMethod,
taskInfo,
destinationBounds.width(),
destinationBounds.height(),
displayController,
)
if (DesktopExperienceFlags.ENABLE_TILE_RESIZING.isTrue()) {
val isTiled = snapEventHandler.snapToHalfScreen(taskInfo, currentDragBounds, position)
if (isTiled) {
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
}
return
}
if (destinationBounds == taskInfo.configuration.windowConfiguration.bounds) {
// Handle the case where we attempt to snap resize when already snap resized: the task
// position won't need to change but we want to animate the surface going back to the
// snapped position from the "dragged-to-the-edge" position.
if (destinationBounds != currentDragBounds && taskSurface != null) {
returnToDragStartAnimator.start(
taskInfo.taskId,
taskSurface,
startBounds = currentDragBounds,
endBounds = destinationBounds,
)
}
return
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true)
val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds)
toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentDragBounds)
}
/**
* Handles snap resizing a [taskInfo] to [position] instantaneously, for example when the
* [resizeTrigger] is the snap resize menu using any [motionEvent] or a keyboard shortcut.
*/
fun handleInstantSnapResizingTask(
taskInfo: RunningTaskInfo,
position: SnapPosition,
resizeTrigger: ResizeTrigger,
inputMethod: InputMethod,
) {
if (!isSnapResizingAllowed(taskInfo)) {
Toast.makeText(
getContext(),
R.string.desktop_mode_non_resizable_snap_text,
Toast.LENGTH_SHORT,
)
.show()
return
}
snapToHalfScreen(
taskInfo,
null,
taskInfo.configuration.windowConfiguration.bounds,
position,
resizeTrigger,
inputMethod,
)
}
@VisibleForTesting
fun handleSnapResizingTaskOnDrag(
taskInfo: RunningTaskInfo,
position: SnapPosition,
taskSurface: SurfaceControl,
currentDragBounds: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
) {
releaseVisualIndicator()
if (!isSnapResizingAllowed(taskInfo)) {
interactionJankMonitor.begin(
taskSurface,
context,
handler,
CUJ_DESKTOP_MODE_SNAP_RESIZE,
"drag_non_resizable",
)
// reposition non-resizable app back to its original position before being dragged
returnToDragStartAnimator.start(
taskInfo.taskId,
taskSurface,
startBounds = currentDragBounds,
endBounds = dragStartBounds,
doOnEnd = {
Toast.makeText(
context,
com.android.wm.shell.R.string.desktop_mode_non_resizable_snap_text,
Toast.LENGTH_SHORT,
)
.show()
},
)
} else {
val resizeTrigger =
if (position == SnapPosition.LEFT) {
ResizeTrigger.DRAG_LEFT
} else {
ResizeTrigger.DRAG_RIGHT
}
interactionJankMonitor.begin(
taskSurface,
context,
handler,
CUJ_DESKTOP_MODE_SNAP_RESIZE,
"drag_resizable",
)
snapToHalfScreen(
taskInfo,
taskSurface,
currentDragBounds,
position,
resizeTrigger,
DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent),
)
}
}
private fun isSnapResizingAllowed(taskInfo: RunningTaskInfo) =
taskInfo.isResizeable || !DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE.isTrue()
private fun getSnapBounds(displayId: Int, position: SnapPosition): Rect {
val displayLayout = displayController.getDisplayLayout(displayId) ?: return Rect()
val stableBounds = Rect().also { displayLayout.getStableBounds(it) }
val destinationWidth = stableBounds.width() / 2
return when (position) {
SnapPosition.LEFT -> {
Rect(
stableBounds.left,
stableBounds.top,
stableBounds.left + destinationWidth,
stableBounds.bottom,
)
}
SnapPosition.RIGHT -> {
Rect(
stableBounds.right - destinationWidth,
stableBounds.top,
stableBounds.right,
stableBounds.bottom,
)
}
}
}
/**
* Get windowing move for a given `taskId`
*
* @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found
*/
@WindowingMode
fun getTaskWindowingMode(taskId: Int): Int {
return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode
?: WINDOWING_MODE_UNDEFINED
}
private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) {
logD(
"prepareForDeskActivation displayId=%d shouldShowHomeBehindDesktop=%b",
displayId,
desktopState.shouldShowHomeBehindDesktop,
)
// Move home to front, ensures that we go back home when all desktop windows are closed
val useParamDisplayId =
DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue
moveHomeTaskToTop(
displayId = if (useParamDisplayId) displayId else context.displayId,
wct = wct,
)
// Currently, we only handle the desktop on the default display really.
if (
(displayId == DEFAULT_DISPLAY ||
ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue) &&
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
!desktopState.shouldShowHomeBehindDesktop
) {
// Add translucent wallpaper activity to show the wallpaper underneath.
addWallpaperActivity(displayId, wct)
}
}
@Deprecated(
"Use addDeskActivationChanges() instead.",
ReplaceWith("addDeskActivationChanges()"),
)
private fun bringDesktopAppsToFront(
displayId: Int,
wct: WindowContainerTransaction,
newTaskIdInFront: Int? = null,
): Int? {
logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront)
prepareForDeskActivation(displayId, wct)
val expandedTasksOrderedFrontToBack = taskRepository.getExpandedTasksOrdered(displayId)
// If we're adding a new Task we might need to minimize an old one
// TODO(b/365725441): Handle non running task minimization
// TODO: b/32994943 - remove dead code when cleaning up task_limit_separate_transition flag
val taskIdToMinimize: Int? =
if (newTaskIdInFront != null) {
getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront)
} else {
null
}
expandedTasksOrderedFrontToBack
// If there is a Task to minimize, let it stay behind the Home Task
.filter { taskId -> taskId != taskIdToMinimize }
.reversed() // Start from the back so the front task is brought forward last
.forEach { taskId ->
val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)
if (runningTaskInfo != null) {
// Task is already running, reorder it to the front
wct.reorder(runningTaskInfo.token, /* onTop= */ true)
} else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) {
// Task is not running, start it
wct.startTask(taskId, createActivityOptionsForStartTask().toBundle())
}
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId)
)
return taskIdToMinimize
}
private fun moveHomeTaskToTop(displayId: Int, wct: WindowContainerTransaction) {
logV("moveHomeTaskToTop in displayId=%d", displayId)
getHomeTask(displayId)?.let { homeTask ->
wct.reorder(homeTask.getToken(), /* onTop= */ true)
}
}
private fun removeHomeTask(wct: WindowContainerTransaction, displayId: Int) {
logV("removeHomeTask in displayId=%d", displayId)
getHomeTask(displayId)?.let { homeTask -> wct.removeRootTask(homeTask.getToken()) }
}
private fun getHomeTask(displayId: Int): RunningTaskInfo? {
return shellTaskOrganizer.getRunningTasks(displayId).firstOrNull { task ->
task.activityType == ACTIVITY_TYPE_HOME
}
}
private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) {
homeIntentProvider.addLaunchHomePendingIntent(wct, displayId, userId)
}
private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) {
logV("addWallpaperActivity")
if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue) {
// If the wallpaper activity for this display already exists, let's reorder it to top.
val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId)
if (wallpaperActivityToken != null) {
wct.reorder(wallpaperActivityToken, /* onTop= */ true)
return
}
val intent = Intent(context, DesktopWallpaperActivity::class.java)
if (ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
val options =
ActivityOptions.makeBasic().apply {
launchWindowingMode = WINDOWING_MODE_FULLSCREEN
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
if (ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue) {
launchDisplayId = displayId
}
}
val pendingIntent =
PendingIntent.getActivity(
context,
/* requestCode = */ 0,
intent,
PendingIntent.FLAG_IMMUTABLE,
)
wct.sendPendingIntent(pendingIntent, intent, options.toBundle())
} else {
val userHandle = UserHandle.of(userId)
val userContext = context.createContextAsUser(userHandle, /* flags= */ 0)
val intent = Intent(userContext, DesktopWallpaperActivity::class.java)
if (
desktopWallpaperActivityTokenProvider.getToken(displayId) == null &&
ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue
) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
intent.putExtra(Intent.EXTRA_USER_HANDLE, userId)
val options =
ActivityOptions.makeBasic().apply {
launchWindowingMode = WINDOWING_MODE_FULLSCREEN
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
if (ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue) {
launchDisplayId = displayId
}
}
val pendingIntent =
PendingIntent.getActivityAsUser(
userContext,
/* requestCode= */ 0,
intent,
PendingIntent.FLAG_IMMUTABLE,
/* options= */ null,
userHandle,
)
wct.sendPendingIntent(pendingIntent, intent, options.toBundle())
}
}
private fun moveWallpaperActivityToBack(wct: WindowContainerTransaction, displayId: Int) {
desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token ->
logV("moveWallpaperActivityToBack")
wct.reorder(token, /* onTop= */ false)
}
}
private fun removeWallpaperTask(wct: WindowContainerTransaction, displayId: Int) {
desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token ->
logV("removeWallpaperTask")
wct.removeTask(token)
}
}
private fun willExitDesktop(
triggerTaskId: Int,
displayId: Int,
forceExitDesktop: Boolean,
): Boolean {
if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
// explicitly going fullscreen, so there's no point in checking the desktop state.
return true
}
if (ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY.isTrue) {
if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) {
return false
}
} else {
if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) {
return false
}
}
return true
}
private fun performDesktopExitCleanupIfNeeded(
taskId: Int,
deskId: Int? = null,
displayId: Int,
wct: WindowContainerTransaction,
forceToFullscreen: Boolean,
): RunOnTransitStart? {
if (!willExitDesktop(taskId, displayId, forceToFullscreen)) {
return null
}
// TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the
// |RunOnTransitStart| when the transition is started.
return performDesktopExitCleanUp(
wct = wct,
deskId = deskId,
displayId = displayId,
willExitDesktop = true,
shouldEndUpAtHome = true,
)
}
/** TODO: b/394268248 - update [deskId] to be non-null. */
fun performDesktopExitCleanUp(
wct: WindowContainerTransaction,
deskId: Int?,
displayId: Int,
willExitDesktop: Boolean,
shouldEndUpAtHome: Boolean = true,
skipWallpaperAndHomeOrdering: Boolean = false,
skipUpdatingExitDesktopListener: Boolean = false,
): RunOnTransitStart? {
if (!willExitDesktop) return null
if (
!skipUpdatingExitDesktopListener &&
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
!desktopState.enableMultipleDesktops
) {
desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted(
FULLSCREEN_ANIMATION_DURATION,
shouldEndUpAtHome,
)
}
if (
!skipWallpaperAndHomeOrdering ||
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
) {
if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue) {
moveWallpaperActivityToBack(wct, displayId)
} else {
removeWallpaperTask(wct, displayId)
}
if (shouldEndUpAtHome) {
// If the transition should end up with user going to home, launch home with a
// pending intent.
addLaunchHomePendingIntent(wct, displayId)
}
}
return prepareDeskDeactivationIfNeeded(wct, deskId)
}
fun releaseVisualIndicator() {
visualIndicator?.releaseVisualIndicator()
visualIndicator = null
}
override fun getContext(): Context = context
override fun getRemoteCallExecutor(): ShellExecutor = mainExecutor
override fun startAnimation(
transition: IBinder,
info: TransitionInfo,
startTransaction: SurfaceControl.Transaction,
finishTransaction: SurfaceControl.Transaction,
finishCallback: Transitions.TransitionFinishCallback,
): Boolean {
// This handler should never be the sole handler, so should not animate anything.
return false
}
private fun taskDisplaySupportDesktopMode(triggerTask: RunningTaskInfo) =
desktopState.isDesktopModeSupportedOnDisplay(triggerTask.displayId)
override fun handleRequest(
transition: IBinder,
request: TransitionRequestInfo,
): WindowContainerTransaction? {
logV("handleRequest request=%s", request)
// First, check if this is a display disconnect request.
val displayChange = request.displayChange
if (
DesktopExperienceFlags.ENABLE_DISPLAY_DISCONNECT_INTERACTION.isTrue &&
displayChange != null &&
displayChange.disconnectReparentDisplay != INVALID_DISPLAY
) {
return onDisplayDisconnect(
displayChange.displayId,
displayChange.disconnectReparentDisplay,
transition,
)
}
// Check if we should skip handling this transition
var reason = ""
val triggerTask = request.triggerTask
// Skipping early if the trigger task is null
if (triggerTask == null) {
logV("skipping handleRequest reason=%s", "triggerTask is null")
return null
}
val recentsAnimationRunning =
RecentsTransitionStateListener.isAnimating(recentsTransitionState)
val shouldHandleMidRecentsFreeformLaunch =
recentsAnimationRunning && isFreeformRelaunch(triggerTask, request)
val isDragAndDropFullscreenTransition = taskContainsDragAndDropCookie(triggerTask)
val shouldHandleRequest =
when {
!taskDisplaySupportDesktopMode(triggerTask) -> {
reason = "triggerTask's display doesn't support desktop mode"
false
}
// Handle freeform relaunch during recents animation
shouldHandleMidRecentsFreeformLaunch -> true
recentsAnimationRunning -> {
reason = "recents animation is running"
false
}
// Don't handle request if this was a tear to fullscreen transition.
// handleFullscreenTaskLaunch moves fullscreen intents to freeform;
// this is an exception to the rule
isDragAndDropFullscreenTransition -> {
dragAndDropFullscreenCookie = null
false
}
// Handle task closing for the last window if wallpaper is available
shouldHandleTaskClosing(request) -> true
// Only handle open or to front transitions
request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> {
reason = "transition type not handled (${request.type})"
false
}
// Home launches are only handled with multiple desktops enabled.
triggerTask.activityType == ACTIVITY_TYPE_HOME &&
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue -> {
reason = "ACTIVITY_TYPE_HOME not handled"
false
}
// Only handle standard and home tasks types.
triggerTask.activityType != ACTIVITY_TYPE_STANDARD &&
triggerTask.activityType != ACTIVITY_TYPE_HOME -> {
reason = "activityType not handled (${triggerTask.activityType})"
false
}
// Only handle fullscreen or freeform tasks
!triggerTask.isFullscreen && !triggerTask.isFreeform -> {
reason = "windowingMode not handled (${triggerTask.windowingMode})"
false
}
// Otherwise process it
else -> true
}
if (!shouldHandleRequest) {
logV("skipping handleRequest reason=%s", reason)
return null
}
val result =
when {
triggerTask.activityType == ACTIVITY_TYPE_HOME ->
handleHomeTaskLaunch(triggerTask, transition)
// Check if freeform task launch during recents should be handled
shouldHandleMidRecentsFreeformLaunch ->
handleMidRecentsFreeformTaskLaunch(triggerTask, transition)
// Check if the closing task needs to be handled
TransitionUtil.isClosingType(request.type) ->
handleTaskClosing(triggerTask, transition, request.type)
// Check if the top task shouldn't be allowed to enter desktop mode
isIncompatibleTask(triggerTask) ->
handleIncompatibleTaskLaunch(triggerTask, transition)
// Check if fullscreen task should be updated
triggerTask.isFullscreen ->
handleFullscreenTaskLaunch(triggerTask, transition, request.type)
// Check if freeform task should be updated
triggerTask.isFreeform -> handleFreeformTaskLaunch(triggerTask, transition)
else -> {
null
}
}
logV("handleRequest result=%s", result)
return result
}
/** Whether the given [change] in the [transition] is a known desktop change. */
fun isDesktopChange(transition: IBinder, change: TransitionInfo.Change): Boolean {
// Only the immersive controller is currently involved in mixed transitions.
return DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue &&
desktopImmersiveController.isImmersiveChange(transition, change)
}
/**
* Whether the given transition [info] will potentially include a desktop change, in which case
* the transition should be treated as mixed so that the change is in part animated by one of
* the desktop transition handlers.
*/
fun shouldPlayDesktopAnimation(info: TransitionRequestInfo): Boolean {
// Only immersive mixed transition are currently supported.
if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return false
val triggerTask = info.triggerTask ?: return false
if (!isAnyDeskActive(triggerTask.displayId)) {
return false
}
if (!TransitionUtil.isOpeningType(info.type)) {
return false
}
taskRepository.getTaskInFullImmersiveState(displayId = triggerTask.displayId)
?: return false
return when {
triggerTask.isFullscreen -> {
// Trigger fullscreen task will enter desktop, so any existing immersive task
// should exit.
shouldFullscreenTaskLaunchSwitchToDesktop(triggerTask, info.type)
}
triggerTask.isFreeform -> {
// Trigger freeform task will enter desktop, so any existing immersive task should
// exit.
!shouldFreeformTaskLaunchSwitchToFullscreen(triggerTask)
}
else -> false
}
}
/** Animate a desktop change found in a mixed transitions. */
fun animateDesktopChange(
transition: IBinder,
change: Change,
startTransaction: Transaction,
finishTransaction: Transaction,
finishCallback: TransitionFinishCallback,
) {
if (!desktopImmersiveController.isImmersiveChange(transition, change)) {
throw IllegalStateException("Only immersive changes support desktop mixed transitions")
}
desktopImmersiveController.animateResizeChange(
change,
startTransaction,
finishTransaction,
finishCallback,
)
}
private fun taskContainsDragAndDropCookie(taskInfo: RunningTaskInfo) =
taskInfo.launchCookies?.any { it == dragAndDropFullscreenCookie } ?: false
/**
* Applies the proper surface states (rounded corners) to tasks when desktop mode is active.
* This is intended to be used when desktop mode is part of another animation but isn't, itself,
* animating.
*/
fun syncSurfaceState(info: TransitionInfo, finishTransaction: SurfaceControl.Transaction) {
// Add rounded corners to freeform windows
if (!desktopConfig.useRoundedCorners) {
return
}
val cornerRadius =
context.resources
.getDimensionPixelSize(
SharedR.dimen.desktop_windowing_freeform_rounded_corner_radius
)
.toFloat()
info.changes
.filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM }
.forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) }
}
/** Returns whether an existing desktop task is being relaunched in freeform or not. */
private fun isFreeformRelaunch(triggerTask: RunningTaskInfo, request: TransitionRequestInfo) =
(triggerTask.windowingMode == WINDOWING_MODE_FREEFORM &&
TransitionUtil.isOpeningType(request.type) &&
taskRepository.isActiveTask(triggerTask.taskId))
/** Returns whether a fullscreen task is being relaunched on the same display or not. */
private fun isFullscreenRelaunch(
triggerTask: RunningTaskInfo,
@WindowManager.TransitionType requestType: Int,
): Boolean {
// Do not treat fullscreen-in-desktop as fullscreen.
if (taskRepository.isActiveTask(triggerTask.taskId)) return false
val existingTask = shellTaskOrganizer.getRunningTaskInfo(triggerTask.taskId) ?: return false
return triggerTask.isFullscreen &&
TransitionUtil.isOpeningType(requestType) &&
existingTask.isFullscreen &&
existingTask.displayId == triggerTask.displayId
}
private fun isIncompatibleTask(task: RunningTaskInfo) =
desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)
private fun shouldHandleTaskClosing(request: TransitionRequestInfo): Boolean =
ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() &&
TransitionUtil.isClosingType(request.type) &&
request.triggerTask != null &&
request.triggerTask?.activityType != ACTIVITY_TYPE_HOME
/** Open an existing instance of an app. */
fun openInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) {
val deskId = getOrCreateDefaultDeskId(callingTask.displayId) ?: return
if (callingTask.isFreeform) {
val requestedTaskInfo = shellTaskOrganizer.getRunningTaskInfo(requestedTaskId)
if (requestedTaskInfo?.isFreeform == true) {
// If requested task is an already open freeform task, just move it to front.
moveTaskToFront(
requestedTaskId,
unminimizeReason = UnminimizeReason.APP_HANDLE_MENU_BUTTON,
)
} else {
moveTaskToDesk(
requestedTaskId,
deskId,
WindowContainerTransaction(),
DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON,
)
}
} else {
val options = createNewWindowOptions(callingTask, deskId)
val splitPosition = splitScreenController.determineNewInstancePosition(callingTask)
splitScreenController.startTask(
requestedTaskId,
splitPosition,
options.toBundle(),
/* hideTaskToken= */ null,
if (enableFlexibleSplit())
splitScreenController.determineNewInstanceIndex(callingTask)
else SPLIT_INDEX_UNDEFINED,
)
}
}
/** Create an Intent to open a new window of a task. */
fun openNewWindow(callingTaskInfo: RunningTaskInfo) {
// TODO(b/337915660): Add a transition handler for these; animations
// need updates in some cases.
val baseActivity = callingTaskInfo.baseActivity ?: return
val userHandle = UserHandle.of(callingTaskInfo.userId)
val fillIn: Intent =
userProfileContexts
.getOrCreate(callingTaskInfo.userId)
.packageManager
.getLaunchIntentForPackage(baseActivity.packageName) ?: return
fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
val launchIntent =
PendingIntent.getActivityAsUser(
context,
/* requestCode= */ 0,
fillIn,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT,
/* options= */ null,
userHandle,
)
val deskId =
taskRepository.getDeskIdForTask(callingTaskInfo.taskId)
?: getOrCreateDefaultDeskId(callingTaskInfo.displayId)
?: return
val options = createNewWindowOptions(callingTaskInfo, deskId)
when (options.launchWindowingMode) {
WINDOWING_MODE_MULTI_WINDOW -> {
val splitPosition =
splitScreenController.determineNewInstancePosition(callingTaskInfo)
// TODO(b/349828130) currently pass in index_undefined until we can revisit these
// specific cases in the future.
val splitIndex =
if (enableFlexibleSplit())
splitScreenController.determineNewInstanceIndex(callingTaskInfo)
else SPLIT_INDEX_UNDEFINED
splitScreenController.startIntent(
launchIntent,
context.userId,
fillIn,
splitPosition,
options.toBundle(),
/* hideTaskToken= */ null,
/* forceLaunchNewTask= */ true,
splitIndex,
if (ENABLE_NON_DEFAULT_DISPLAY_SPLIT.isTrue) callingTaskInfo.displayId
else DEFAULT_DISPLAY,
)
}
WINDOWING_MODE_FREEFORM -> {
val wct = WindowContainerTransaction()
wct.sendPendingIntent(launchIntent, fillIn, options.toBundle())
startLaunchTransition(
transitionType = TRANSIT_OPEN,
wct = wct,
launchingTaskId = null,
deskId = deskId,
displayId = callingTaskInfo.displayId,
)
}
}
}
private fun createNewWindowOptions(callingTask: RunningTaskInfo, deskId: Int): ActivityOptions {
val newTaskWindowingMode =
when {
callingTask.isFreeform -> {
WINDOWING_MODE_FREEFORM
}
callingTask.isFullscreen || callingTask.isMultiWindow -> {
WINDOWING_MODE_MULTI_WINDOW
}
else -> {
error("Invalid windowing mode: ${callingTask.windowingMode}")
}
}
val bounds =
when (newTaskWindowingMode) {
WINDOWING_MODE_FREEFORM -> {
displayController.getDisplayLayout(callingTask.displayId)?.let {
getInitialBounds(it, callingTask, deskId)
}
}
WINDOWING_MODE_MULTI_WINDOW -> {
Rect()
}
else -> {
error("Invalid windowing mode: $newTaskWindowingMode")
}
}
val displayId =
if (ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue)
taskRepository.getDisplayForDesk(deskId)
else DEFAULT_DISPLAY
return ActivityOptions.makeBasic().apply {
launchWindowingMode = newTaskWindowingMode
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
launchBounds = bounds
launchDisplayId = displayId
}
}
private fun handleHomeTaskLaunch(
task: RunningTaskInfo,
transition: IBinder,
): WindowContainerTransaction? {
logV(
"DesktopTasksController: handleHomeTaskLaunch taskId=%s userId=%s currentUserId=%d",
task.taskId,
task.userId,
userId,
)
// On user-switches, the home task is launched and the request is dispatched before the
// user-switch is known by SysUI/Shell, so don't use the "current" repository.
val repository = userRepositories.getProfile(task.userId)
val activeDeskId = repository.getActiveDeskId(task.displayId) ?: return null
val wct = WindowContainerTransaction()
// TODO: b/393978539 - desktop-first displays may need to keep the desk active.
// TODO: b/415381304 - pass in the correct |userId| to |performDesktopExitCleanUp| to
// ensure desk deactivation updates are applied to the right repository.
val runOnTransitStart =
performDesktopExitCleanUp(
wct = wct,
deskId = activeDeskId,
displayId = task.displayId,
willExitDesktop = true,
shouldEndUpAtHome = true,
// No need to clean up the wallpaper / home order if Home is launching directly.
skipWallpaperAndHomeOrdering = true,
)
runOnTransitStart?.invoke(transition)
return wct
}
/**
* Handles the case where a freeform task is launched from recents.
*
* This is a special case where we want to launch the task in fullscreen instead of freeform.
*/
private fun handleMidRecentsFreeformTaskLaunch(
task: RunningTaskInfo,
transition: IBinder,
): WindowContainerTransaction? {
logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch")
val wct = WindowContainerTransaction()
val runOnTransitStart =
addMoveToFullscreenChanges(
wct = wct,
taskInfo = task,
willExitDesktop =
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
forceExitDesktop = true,
),
)
runOnTransitStart?.invoke(transition)
wct.reorder(task.token, true)
return wct
}
private fun handleFreeformTaskLaunch(
task: RunningTaskInfo,
transition: IBinder,
): WindowContainerTransaction? {
val anyDeskActive = taskRepository.isAnyDeskActive(task.displayId)
val forceEnterDesktop = forceEnterDesktop(task.displayId)
logV(
"handleFreeformTaskLaunch taskId=%d displayId=%d anyDeskActive=%b forceEnterDesktop=%b",
task.taskId,
task.displayId,
anyDeskActive,
forceEnterDesktop,
)
if (keyguardManager.isKeyguardLocked) {
// Do NOT handle freeform task launch when locked.
// It will be launched in fullscreen windowing mode (Details: b/160925539)
logV("skip keyguard is locked")
return null
}
val deskId = getOrCreateDefaultDeskId(task.displayId) ?: return null
val isKnownDesktopTask = taskRepository.isActiveTask(task.taskId)
val shouldEnterDesktop =
forceEnterDesktop
// New tasks should be forced into desktop, while known desktop tasks should
// be moved outside of desktop.
|| !isKnownDesktopTask
logV(
"handleFreeformTaskLaunch taskId=%d displayId=%d anyDeskActive=%b" +
" isKnownDesktopTask=%b shouldEnterDesktop=%b",
task.taskId,
task.displayId,
anyDeskActive,
isKnownDesktopTask,
shouldEnterDesktop,
)
val wct = WindowContainerTransaction()
if (!anyDeskActive && !shouldEnterDesktop) {
// We are outside of desktop mode and an already existing desktop task is being
// launched. We should make this task go to fullscreen instead of freeform. Note
// that this means any re-launch of a freeform window outside of desktop will be in
// fullscreen as long as default-desktop flag is disabled.
val runOnTransitStart =
addMoveToFullscreenChanges(
wct = wct,
taskInfo = task,
willExitDesktop = false, // Already outside desktop.
)
runOnTransitStart?.invoke(transition)
return wct
}
// At this point we're either already in desktop and this task is moving to it, or we're
// about to enter desktop with this task in it.
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
// Make sure the launching task is moved into the desk.
desksOrganizer.moveTaskToDesk(wct, deskId, task)
}
if (!anyDeskActive) {
// We are outside of desktop and should enter desktop.
val runOnTransitStart = addDeskActivationChanges(deskId, wct, task)
runOnTransitStart?.invoke(transition)
wct.reorder(task.token, true)
return wct
}
// We were already in desktop.
val inheritedTaskBounds =
getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
if (!taskRepository.isActiveTask(task.taskId) && inheritedTaskBounds != null) {
// Inherit bounds from closing task instance to prevent application jumping different
// cascading positions.
wct.setBounds(task.token, inheritedTaskBounds)
}
// TODO(b/365723620): Handle non running tasks that were launched after reboot.
// If task is already visible, it must have been handled already and added to desktop mode.
// Cascade task only if it's not visible yet and has no inherited bounds.
if (
inheritedTaskBounds == null &&
DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue() &&
!taskRepository.isVisibleTask(task.taskId)
) {
val displayLayout = displayController.getDisplayLayout(task.displayId)
if (displayLayout != null) {
val stableBounds = Rect().also { displayLayout.getStableBounds(it) }
val initialBounds = Rect(task.configuration.windowConfiguration.bounds)
cascadeWindow(initialBounds, displayLayout, deskId, stableBounds)
wct.setBounds(task.token, initialBounds)
}
}
if (desktopConfig.useDesktopOverrideDensity) {
wct.setDensityDpi(task.token, desktopConfig.desktopDensityOverride)
}
// The task that is launching might have been minimized before - in which case this is an
// unminimize action.
if (taskRepository.isMinimizedTask(task.taskId)) {
addPendingUnminimizeTransition(
transition,
task.displayId,
task.taskId,
UnminimizeReason.TASK_LAUNCH,
)
}
// Desktop Mode is showing and we're launching a new Task:
// 1) Exit immersive if needed.
desktopImmersiveController.exitImmersiveIfApplicable(
transition = transition,
wct = wct,
displayId = task.displayId,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
// 2) minimize a Task if needed.
// TODO: b/32994943 - remove dead code when cleaning up task_limit_separate_transition flag
val taskIdToMinimize = addAndGetMinimizeChanges(deskId, wct, task.taskId)
// 3) Remove top transparent fullscreen task if needed.
val closingTopTransparentTaskId =
taskRepository.getTopTransparentFullscreenTaskData(deskId)?.taskId
closeTopTransparentFullscreenTask(wct, deskId)
addPendingAppLaunchTransition(
transition,
task.taskId,
taskIdToMinimize,
closingTopTransparentTaskId,
)
if (taskIdToMinimize != null) {
addPendingMinimizeTransition(transition, taskIdToMinimize, MinimizeReason.TASK_LIMIT)
snapEventHandler.removeTaskIfTiled(task.displayId, taskIdToMinimize)
return wct
}
addPendingTaskLimitTransition(transition, deskId, task.taskId)
if (!wct.isEmpty) {
return wct
}
return null
}
private fun handleFullscreenTaskLaunch(
task: RunningTaskInfo,
transition: IBinder,
@WindowManager.TransitionType requestType: Int,
): WindowContainerTransaction? {
logV("handleFullscreenTaskLaunch")
if (shouldFullscreenTaskLaunchSwitchToDesktop(task, requestType)) {
logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId)
return WindowContainerTransaction().also { wct ->
val deskId = getOrCreateDefaultDeskId(task.displayId) ?: return@also
addMoveToDeskTaskChanges(wct = wct, task = task, deskId = deskId)
val runOnTransitStart: RunOnTransitStart? =
if (
task.baseIntent.flags.and(Intent.FLAG_ACTIVITY_TASK_ON_HOME) != 0 ||
!isAnyDeskActive(task.displayId)
) {
// In some launches home task is moved behind new task being launched. Make
// sure that's not the case for launches in desktop. Also, if this launch is
// the first one to trigger the desktop mode (e.g., when
// [forceEnterDesktop()]), activate the desk here.
val activationRunnable =
addDeskActivationChanges(
deskId = deskId,
wct = wct,
newTask = task,
addPendingLaunchTransition = true,
)
wct.reorder(task.token, true)
activationRunnable
} else {
{ transition: IBinder ->
// The desk was already showing and we're launching a new Task - we
// might need to minimize another Task.
// TODO: b/32994943 - remove dead code when cleaning up
// task_limit_separate_transition flag
val taskIdToMinimize =
addAndGetMinimizeChanges(deskId, wct, task.taskId)
taskIdToMinimize?.let { minimizingTaskId ->
addPendingMinimizeTransition(
transition,
minimizingTaskId,
MinimizeReason.TASK_LIMIT,
)
}
addPendingTaskLimitTransition(transition, deskId, task.taskId)
// Remove top transparent fullscreen task if needed.
val closingTopTransparentTaskId =
taskRepository.getTopTransparentFullscreenTaskData(deskId)?.taskId
closeTopTransparentFullscreenTask(wct, deskId)
// Also track the pending launching task.
addPendingAppLaunchTransition(
transition,
task.taskId,
taskIdToMinimize,
closingTopTransparentTaskId,
)
}
}
runOnTransitStart?.invoke(transition)
desktopImmersiveController.exitImmersiveIfApplicable(
transition,
wct,
task.displayId,
reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH,
)
}
} else if (taskRepository.isActiveTask(task.taskId)) {
// If a freeform task receives a request for a fullscreen launch, apply the same
// changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED
// set when needed can interfere with future split / multi-instance transitions.
val wct = WindowContainerTransaction()
val runOnTransitStart =
addMoveToFullscreenChanges(
wct = wct,
taskInfo = task,
willExitDesktop =
willExitDesktop(
triggerTaskId = task.taskId,
displayId = task.displayId,
forceExitDesktop = true,
),
)
runOnTransitStart?.invoke(transition)
return wct
}
return null
}
private fun shouldFreeformTaskLaunchSwitchToFullscreen(task: RunningTaskInfo): Boolean =
!isAnyDeskActive(task.displayId)
private fun shouldFullscreenTaskLaunchSwitchToDesktop(
task: RunningTaskInfo,
@WindowManager.TransitionType requestType: Int,
): Boolean {
val isDesktopFirst = rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(task.displayId)
if (
DesktopExperienceFlags.ENABLE_DESKTOP_FIRST_FULLSCREEN_REFOCUS_BUGFIX.isTrue &&
isDesktopFirst &&
isFullscreenRelaunch(task, requestType)
) {
logV(
"shouldFullscreenTaskLaunchSwitchToDesktop: no switch as fullscreen relaunch on" +
" desktop-first display#%s",
task.displayId,
)
return false
}
val isAnyDeskActive = isAnyDeskActive(task.displayId)
logV(
"shouldFullscreenTaskLaunchSwitchToDesktop, isAnyDeskActive=%s, isDesktopFirst=%s",
isAnyDeskActive,
isDesktopFirst,
)
return isAnyDeskActive || isDesktopFirst
}
/**
* If a task is not compatible with desktop mode freeform, it should always be launched in
* fullscreen.
*/
private fun handleIncompatibleTaskLaunch(
task: RunningTaskInfo,
transition: IBinder,
): WindowContainerTransaction? {
val taskId = task.taskId
val displayId = task.displayId
val inDesktop = isAnyDeskActive(displayId)
val isTransparentTask = desktopModeCompatPolicy.isTransparentTask(task)
val isFreeform = task.isFreeform
logV(
"handleIncompatibleTaskLaunch taskId=%d displayId=%d isTransparent=%b inDesktop=%b" +
" isFreeform=%b",
taskId,
displayId,
isTransparentTask,
inDesktop,
isFreeform,
)
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
if (!inDesktop && !forceEnterDesktop(displayId)) return null
if (
isTransparentTask &&
(DesktopExperienceFlags.FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK.isTrue ||
DesktopModeFlags
.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
.isTrue)
) {
// Only update task repository for transparent task.
val deskId = taskRepository.getActiveDeskId(displayId)
deskId?.let { taskRepository.setTopTransparentFullscreenTaskData(it, task) }
}
// Already fullscreen, no-op.
if (task.isFullscreen) return null
val wct = WindowContainerTransaction()
val runOnTransitStart =
addMoveToFullscreenChanges(
wct = wct,
taskInfo = task,
willExitDesktop =
willExitDesktop(
triggerTaskId = taskId,
displayId = displayId,
forceExitDesktop = true,
),
)
runOnTransitStart?.invoke(transition)
return wct
}
if (!inDesktop && !isFreeform) {
logD("handleIncompatibleTaskLaunch not in desktop, not a freeform task, nothing to do")
return null
}
if (
isTransparentTask &&
(DesktopExperienceFlags.FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK.isTrue ||
DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC
.isTrue)
) {
// Only update task repository for transparent task.
val deskId = taskRepository.getActiveDeskId(displayId)
deskId?.let { taskRepository.setTopTransparentFullscreenTaskData(it, task) }
}
// Both opaque and transparent incompatible tasks need to be forced to fullscreen, but
// opaque ones force-exit the desktop while transparent ones are just shown on top of the
// desktop while keeping it active.
val willExitDesktop = inDesktop && !isTransparentTask
if (willExitDesktop) {
logD("handleIncompatibleTaskLaunch forcing task to fullscreen and exiting desktop")
} else {
logD("handleIncompatibleTaskLaunch forcing task to fullscreen and staying in desktop")
}
val wct = WindowContainerTransaction()
val runOnTransitStart =
addMoveToFullscreenChanges(
wct = wct,
taskInfo = task,
willExitDesktop = willExitDesktop,
)
runOnTransitStart?.invoke(transition)
return wct
}
/**
* Handles a closing task. This usually means deactivating and cleaning up the desk if it was
* the last task in it. It also handles to-back transitions of the last desktop task as a
* minimize operation.
*/
private fun handleTaskClosing(
task: RunningTaskInfo,
transition: IBinder,
@WindowManager.TransitionType requestType: Int,
): WindowContainerTransaction? {
logV(
"handleTaskClosing taskId=%d closingType=%s",
task.taskId,
transitTypeToString(requestType),
)
if (!isAnyDeskActive(task.displayId)) return null
val deskId = taskRepository.getDeskIdForTask(task.taskId)
if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
return null
}
val wct = WindowContainerTransaction()
if (
DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
requestType == TRANSIT_TO_BACK
) {
val isLastTask = taskRepository.isOnlyVisibleTask(task.taskId, task.displayId)
logV(
"Handling to-back of taskId=%d (isLast=%b) as minimize in deskId=%d",
task.taskId,
isLastTask,
deskId,
)
desksOrganizer.minimizeTask(
wct = wct,
deskId = checkNotNull(deskId) { "Expected non-null deskId" },
task = task,
)
}
// TODO(b/416014060): Check if task is really receiving a back gesture
if (
!(DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
DesktopExperienceFlags.ENABLE_EMPTY_DESK_ON_MINIMIZE.isTrue)
) {
val deactivationRunnable =
performDesktopExitCleanupIfNeeded(
taskId = task.taskId,
deskId = deskId,
displayId = task.displayId,
wct = wct,
forceToFullscreen = false,
)
deactivationRunnable?.invoke(transition)
}
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) {
taskRepository.addClosingTask(
displayId = task.displayId,
deskId = deskId,
taskId = task.taskId,
)
snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId)
}
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(task.displayId, task.taskId)
)
return if (wct.isEmpty) null else wct
}
/**
* Applies the [wct] changes need when a task is first moving to a desk and the desk needs to be
* activated.
*/
private fun addDeskActivationWithMovingTaskChanges(
deskId: Int,
wct: WindowContainerTransaction,
task: RunningTaskInfo,
): RunOnTransitStart? {
val runOnTransitStart = addDeskActivationChanges(deskId, wct, task)
addMoveToDeskTaskChanges(wct = wct, task = task, deskId = deskId)
return runOnTransitStart
}
/**
* Applies the [wct] changes needed when a task is first moving to a desk.
*
* Note that this recalculates the initial bounds of the task, so it should not be used when
* transferring a task between desks.
*
* TODO: b/362720497 - this should be improved to be reusable by desk-to-desk CUJs where
* [DesksOrganizer.moveTaskToDesk] needs to be called and even cross-display CUJs where
* [applyFreeformDisplayChange] needs to be called. Potentially by comparing source vs
* destination desk ids and display ids, or adding extra arguments to the function.
*/
fun addMoveToDeskTaskChanges(
wct: WindowContainerTransaction,
task: RunningTaskInfo,
deskId: Int,
) {
val targetDisplayId = taskRepository.getDisplayForDesk(deskId)
val displayLayout = displayController.getDisplayLayout(targetDisplayId) ?: return
logV(
"addMoveToDeskTaskChanges taskId=%d deskId=%d displayId=%d",
task.taskId,
deskId,
targetDisplayId,
)
val inheritedTaskBounds =
getInheritedExistingTaskBounds(taskRepository, shellTaskOrganizer, task, deskId)
if (inheritedTaskBounds != null) {
// Inherit bounds from closing task instance to prevent application jumping different
// cascading positions.
wct.setBounds(task.token, inheritedTaskBounds)
} else {
val initialBounds = getInitialBounds(displayLayout, task, deskId)
if (canChangeTaskPosition(task)) {
wct.setBounds(task.token, initialBounds)
}
}
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.moveTaskToDesk(wct = wct, deskId = deskId, task = task)
} else {
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(targetDisplayId)!!
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val targetWindowingMode =
if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
// Display windowing is freeform, set to undefined and inherit it
WINDOWING_MODE_UNDEFINED
} else {
WINDOWING_MODE_FREEFORM
}
wct.setWindowingMode(task.token, targetWindowingMode)
wct.reorder(task.token, /* onTop= */ true)
}
if (desktopConfig.useDesktopOverrideDensity) {
wct.setDensityDpi(task.token, desktopConfig.desktopDensityOverride)
}
}
/**
* Apply changes to move a freeform task from one display to another, which includes handling
* density changes between displays.
*/
private fun applyFreeformDisplayChange(
wct: WindowContainerTransaction,
taskInfo: RunningTaskInfo,
destDisplayId: Int,
destDeskId: Int,
) {
val sourceLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
val destLayout = displayController.getDisplayLayout(destDisplayId) ?: return
val bounds = taskInfo.configuration.windowConfiguration.bounds
val scaledWidth = bounds.width() * destLayout.densityDpi() / sourceLayout.densityDpi()
val scaledHeight = bounds.height() * destLayout.densityDpi() / sourceLayout.densityDpi()
val sourceWidthMargin = sourceLayout.width() - bounds.width()
val sourceHeightMargin = sourceLayout.height() - bounds.height()
val destWidthMargin = destLayout.width() - scaledWidth
val destHeightMargin = destLayout.height() - scaledHeight
val scaledLeft =
if (sourceWidthMargin != 0) {
bounds.left * destWidthMargin / sourceWidthMargin
} else {
destWidthMargin / 2
}
val scaledTop =
if (sourceHeightMargin != 0) {
bounds.top * destHeightMargin / sourceHeightMargin
} else {
destHeightMargin / 2
}
val boundsWithinDisplay =
if (destWidthMargin >= 0 && destHeightMargin >= 0) {
Rect(0, 0, scaledWidth, scaledHeight).apply {
offsetTo(
scaledLeft.coerceIn(0, destWidthMargin),
scaledTop.coerceIn(0, destHeightMargin),
)
}
} else {
getInitialBounds(destLayout, taskInfo, destDeskId)
}
wct.setBounds(taskInfo.token, boundsWithinDisplay)
}
private fun getInitialBounds(
displayLayout: DisplayLayout,
taskInfo: RunningTaskInfo,
deskId: Int,
): Rect {
val bounds =
if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue) {
// If caption insets should be excluded from app bounds, ensure caption insets
// are excluded from the ideal initial bounds when scaling non-resizeable apps.
// Caption insets stay fixed and don't scale with bounds.
val displayId = taskRepository.getDisplayForDesk(deskId)
val displayContext = displayController.getDisplayContext(displayId) ?: context
val captionInsets =
if (desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo)) {
getDesktopViewAppHeaderHeightPx(displayContext)
} else {
0
}
calculateInitialBounds(displayLayout, taskInfo, captionInsets = captionInsets)
} else {
calculateDefaultDesktopTaskBounds(displayLayout)
}
if (DesktopModeFlags.ENABLE_CASCADING_WINDOWS.isTrue) {
cascadeWindow(bounds, displayLayout, deskId)
}
return bounds
}
/** Applies the changes needed to enter fullscreen and clean up the desktop if needed. */
private fun addMoveToFullscreenChanges(
wct: WindowContainerTransaction,
taskInfo: TaskInfo,
willExitDesktop: Boolean,
displayId: Int = taskInfo.displayId,
): RunOnTransitStart? {
val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)!!
val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
val targetWindowingMode =
if (tdaWindowingMode == WINDOWING_MODE_FULLSCREEN) {
// Display windowing is fullscreen, set to undefined and inherit it
WINDOWING_MODE_UNDEFINED
} else {
WINDOWING_MODE_FULLSCREEN
}
wct.setWindowingMode(taskInfo.token, targetWindowingMode)
wct.setBounds(taskInfo.token, Rect())
if (desktopConfig.useDesktopOverrideDensity) {
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true)
} else if (enableAltTabKqsFlatenning.isTrue) {
// Until multiple desktops is enabled, we still want to reorder the task to top so that
// if the task is not on top we can still switch to it using Alt+Tab.
wct.reorder(taskInfo.token, /* onTop= */ true)
}
val deskId =
taskRepository.getDeskIdForTask(taskInfo.taskId)
?: if (enableAltTabKqsFlatenning.isTrue) {
taskRepository.getActiveDeskId(displayId)
} else {
null
}
return performDesktopExitCleanUp(
wct = wct,
deskId = deskId,
displayId = displayId,
willExitDesktop = willExitDesktop,
shouldEndUpAtHome = false,
)
}
private fun cascadeWindow(
bounds: Rect,
displayLayout: DisplayLayout,
deskId: Int,
stableBounds: Rect = Rect(),
) {
if (stableBounds.isEmpty) {
displayLayout.getStableBoundsForDesktopMode(stableBounds)
}
val activeTasks = taskRepository.getExpandedTasksIdsInDeskOrdered(deskId)
activeTasks
.firstOrNull { !taskRepository.isClosingTask(it) }
?.let { activeTask ->
shellTaskOrganizer.getRunningTaskInfo(activeTask)?.let {
cascadeWindow(
context.resources,
stableBounds,
it.configuration.windowConfiguration.bounds,
bounds,
)
}
}
}
/**
* Adds split screen changes to a transaction. Note that bounds are not reset here due to
* animation; see {@link onDesktopSplitSelectAnimComplete}
*/
private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) {
if (!DesktopModeFlags.ENABLE_INPUT_LAYER_TRANSITION_FIX.isTrue) {
// This windowing mode is to get the transition animation started; once we complete
// split select, we will change windowing mode to undefined and inherit from split
// stage.
// Going to undefined here causes task to flicker to the top left.
// Cancelling the split select flow will revert it to fullscreen.
wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
}
// The task's density may have been overridden in freeform; revert it here as we don't
// want it overridden in multi-window.
wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi())
}
/** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */
private fun addAndGetMinimizeChanges(
deskId: Int,
wct: WindowContainerTransaction,
newTaskId: Int?,
launchingNewIntent: Boolean = false,
): Int? {
if (DesktopExperienceFlags.ENABLE_DESKTOP_TASK_LIMIT_SEPARATE_TRANSITION.isTrue) return null
val limiter = desktopTasksLimiter.getOrNull() ?: return null
require(newTaskId == null || !launchingNewIntent)
return limiter.addAndGetMinimizeTaskChanges(deskId, wct, newTaskId, launchingNewIntent)
}
private fun getTaskIdToMinimize(
expandedTasksOrderedFrontToBack: List<Int>,
newTaskIdInFront: Int?,
): Int? {
if (DesktopExperienceFlags.ENABLE_DESKTOP_TASK_LIMIT_SEPARATE_TRANSITION.isTrue) return null
val limiter = desktopTasksLimiter.getOrNull() ?: return null
return limiter.getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront)
}
private fun addPendingMinimizeTransition(
transition: IBinder,
taskIdToMinimize: Int,
minimizeReason: MinimizeReason,
) {
val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize)
desktopTasksLimiter.ifPresent {
it.addPendingMinimizeChange(
transition = transition,
displayId = taskToMinimize?.displayId ?: DEFAULT_DISPLAY,
taskId = taskIdToMinimize,
minimizeReason = minimizeReason,
)
}
}
private fun addPendingTaskLimitTransition(
transition: IBinder,
deskId: Int,
launchTaskId: Int?,
) {
if (!DesktopExperienceFlags.ENABLE_DESKTOP_TASK_LIMIT_SEPARATE_TRANSITION.isTrue) return
desktopTasksLimiter.ifPresent {
it.addPendingTaskLimitTransition(
transition = transition,
deskId = deskId,
taskId = launchTaskId,
)
}
}
private fun addPendingUnminimizeTransition(
transition: IBinder,
displayId: Int,
taskIdToUnminimize: Int,
unminimizeReason: UnminimizeReason,
) {
desktopTasksLimiter.ifPresent {
it.addPendingUnminimizeChange(
transition,
displayId = displayId,
taskId = taskIdToUnminimize,
unminimizeReason,
)
}
}
private fun addPendingAppLaunchTransition(
transition: IBinder,
launchTaskId: Int,
minimizeTaskId: Int?,
closingTopTransparentTaskId: Int?,
) {
if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue) {
return
}
// TODO b/359523924: pass immersive task here?
desktopMixedTransitionHandler.addPendingMixedTransition(
DesktopMixedTransitionHandler.PendingMixedTransition.Launch(
transition,
launchTaskId,
minimizeTaskId,
closingTopTransparentTaskId,
/* exitingImmersiveTask= */ null,
)
)
}
private fun activateDefaultDeskInDisplay(
displayId: Int,
remoteTransition: RemoteTransition? = null,
taskIdToReorderToFront: Int? = null,
) {
val deskId = getOrCreateDefaultDeskId(displayId) ?: return
activateDesk(deskId, remoteTransition, taskIdToReorderToFront)
}
/**
* Applies the necessary [wct] changes to activate the given desk.
*
* When a task is being brought into a desk together with the activation, then [newTask] is not
* null and may be used to run other desktop policies, such as minimizing another task if the
* task limit has been exceeded.
*/
fun addDeskActivationChanges(
deskId: Int,
wct: WindowContainerTransaction,
newTask: TaskInfo? = null,
// TODO: b/362720497 - should this be true in other places? Can it be calculated locally
// without having to specify the value?
addPendingLaunchTransition: Boolean = false,
displayId: Int = taskRepository.getDisplayForDesk(deskId),
): RunOnTransitStart {
val newTaskIdInFront = newTask?.taskId
logV(
"addDeskActivationChanges newTaskId=%d deskId=%d displayId=%d",
newTask?.taskId,
deskId,
displayId,
)
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
val taskIdToMinimize = bringDesktopAppsToFront(displayId, wct, newTask?.taskId)
return { transition ->
// TODO: b/32994943 - remove dead code when cleaning up
// task_limit_separate_transition flag
taskIdToMinimize?.let { minimizingTaskId ->
addPendingMinimizeTransition(
transition = transition,
taskIdToMinimize = minimizingTaskId,
minimizeReason = MinimizeReason.TASK_LIMIT,
)
}
addPendingTaskLimitTransition(
transition = transition,
deskId = deskId,
launchTaskId = newTask?.taskId,
)
if (newTask != null && addPendingLaunchTransition) {
// Remove top transparent fullscreen task if needed.
val closingTopTransparentTaskId =
taskRepository.getTopTransparentFullscreenTaskData(deskId)?.taskId
closeTopTransparentFullscreenTask(wct, deskId)
addPendingAppLaunchTransition(
transition,
newTask.taskId,
taskIdToMinimize,
closingTopTransparentTaskId,
)
}
}
}
prepareForDeskActivation(displayId, wct)
desksOrganizer.activateDesk(wct, deskId)
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(displayId)
)
val expandedTasksOrderedFrontToBack =
taskRepository.getExpandedTasksIdsInDeskOrdered(deskId = deskId)
// If we're adding a new Task we might need to minimize an old one
// TODO: b/32994943 - remove dead code when cleaning up task_limit_separate_transition flag
val taskIdToMinimize =
getTaskIdToMinimize(expandedTasksOrderedFrontToBack, newTaskIdInFront)
if (taskIdToMinimize != null) {
val taskToMinimize = shellTaskOrganizer.getRunningTaskInfo(taskIdToMinimize)
// TODO(b/365725441): Handle non running task minimization
if (taskToMinimize != null) {
desksOrganizer.minimizeTask(wct, deskId, taskToMinimize)
}
}
if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) {
expandedTasksOrderedFrontToBack
.filter { taskId -> taskId != taskIdToMinimize }
.reversed()
.forEach { taskId ->
val runningTaskInfo = shellTaskOrganizer.getRunningTaskInfo(taskId)
if (runningTaskInfo == null) {
wct.startTask(taskId, createActivityOptionsForStartTask().toBundle())
} else {
desksOrganizer.reorderTaskToFront(wct, deskId, runningTaskInfo)
}
}
}
val deactivatingDesk = taskRepository.getActiveDeskId(displayId)?.takeIf { it != deskId }
val deactivationRunnable = prepareDeskDeactivationIfNeeded(wct, deactivatingDesk)
return { transition ->
val activateDeskTransition =
if (newTaskIdInFront != null) {
DeskTransition.ActivateDeskWithTask(
token = transition,
displayId = displayId,
deskId = deskId,
enterTaskId = newTaskIdInFront,
runOnTransitEnd = { snapEventHandler.onDeskActivated(deskId, displayId) },
)
} else {
DeskTransition.ActivateDesk(
token = transition,
displayId = displayId,
deskId = deskId,
runOnTransitEnd = { snapEventHandler.onDeskActivated(deskId, displayId) },
)
}
desksTransitionObserver.addPendingTransition(activateDeskTransition)
taskIdToMinimize?.let { minimizingTask ->
addPendingMinimizeTransition(transition, minimizingTask, MinimizeReason.TASK_LIMIT)
}
addPendingTaskLimitTransition(transition, deskId, newTask?.taskId)
deactivationRunnable?.invoke(transition)
}
}
private fun closeTopTransparentFullscreenTask(wct: WindowContainerTransaction, deskId: Int) {
if (!DesktopExperienceFlags.FORCE_CLOSE_TOP_TRANSPARENT_FULLSCREEN_TASK.isTrue) return
val data = taskRepository.getTopTransparentFullscreenTaskData(deskId)
if (data != null) {
logD("closeTopTransparentFullscreenTask: taskId=%d, deskId=%d", data.taskId, deskId)
wct.removeTask(data.token)
}
}
/** Activates the desk at the given index if it exists. */
fun activatePreviousDesk(displayId: Int) {
if (
!DesktopExperienceFlags.ENABLE_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS.isTrue ||
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
) {
return
}
val validDisplay =
when {
displayId != INVALID_DISPLAY -> displayId
focusTransitionObserver.globallyFocusedDisplayId != INVALID_DISPLAY ->
focusTransitionObserver.globallyFocusedDisplayId
else -> {
logW("activatePreviousDesk no valid display found")
return
}
}
val activeDeskId = taskRepository.getActiveDeskId(validDisplay)
if (activeDeskId == null) {
logV("activatePreviousDesk no active desk in display=%d", validDisplay)
return
}
val destinationDeskId = taskRepository.getPreviousDeskId(activeDeskId)
if (destinationDeskId == null) {
logV(
"activatePreviousDesk no previous desk before deskId=%d in display=%d",
activeDeskId,
validDisplay,
)
// TODO: b/389957556 - add animation.
return
}
logV("activatePreviousDesk from deskId=%d to deskId=%d", activeDeskId, destinationDeskId)
activateDesk(destinationDeskId)
}
/** Activates the desk at the given index if it exists. */
fun activateNextDesk(displayId: Int) {
if (
!DesktopExperienceFlags.ENABLE_KEYBOARD_SHORTCUTS_TO_SWITCH_DESKS.isTrue ||
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
) {
return
}
val validDisplay =
when {
displayId != INVALID_DISPLAY -> displayId
focusTransitionObserver.globallyFocusedDisplayId != INVALID_DISPLAY ->
focusTransitionObserver.globallyFocusedDisplayId
else -> {
logW("activateNextDesk no valid display found")
return
}
}
val activeDeskId = taskRepository.getActiveDeskId(validDisplay)
if (activeDeskId == null) {
logV("activateNextDesk no active desk in display=%d", validDisplay)
return
}
val destinationDeskId = taskRepository.getNextDeskId(activeDeskId)
if (destinationDeskId == null) {
logV(
"activateNextDesk no next desk before deskId=%d in display=%d",
activeDeskId,
validDisplay,
)
// TODO: b/389957556 - add animation.
return
}
logV("activateNextDesk from deskId=%d to deskId=%d", activeDeskId, destinationDeskId)
activateDesk(destinationDeskId)
}
/**
* Activates the given desk and brings [taskIdToReorderToFront] to front if provided and is
* already on the given desk.
*/
fun activateDesk(
deskId: Int,
remoteTransition: RemoteTransition? = null,
taskIdToReorderToFront: Int? = null,
) {
logD(
"activateDesk deskId=%d taskIdToReorderToFront=%d remoteTransition=%s",
deskId,
taskIdToReorderToFront,
remoteTransition,
)
if (!taskRepository.getAllDeskIds().contains(deskId)) {
logW(
"Request to activate desk=%d but desk not found for user=%d",
deskId,
taskRepository.userId,
)
return
}
if (
taskIdToReorderToFront != null &&
taskRepository.getDeskIdForTask(taskIdToReorderToFront) != deskId
) {
logW(
"activeDesk taskIdToReorderToFront=%d not on the desk %d",
taskIdToReorderToFront,
deskId,
)
return
}
val newTaskInFront =
taskIdToReorderToFront?.let { taskId ->
shellTaskOrganizer.getRunningTaskInfo(taskId)
?: recentTasksController?.findTaskInBackground(taskId)
}
val wct = WindowContainerTransaction()
val runOnTransitStart = addDeskActivationChanges(deskId, wct, newTaskInFront)
// Put task with [taskIdToReorderToFront] to front.
when (newTaskInFront) {
is RunningTaskInfo -> {
// Task is running, reorder it.
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.reorderTaskToFront(wct, deskId, newTaskInFront)
} else {
wct.reorder(newTaskInFront.token, /* onTop= */ true)
}
}
is RecentTaskInfo -> {
// Task is not running, start it.
wct.startTask(
taskIdToReorderToFront,
createActivityOptionsForStartTask().toBundle(),
)
}
else -> {
logW("activateDesk taskIdToReorderToFront=%d not found", taskIdToReorderToFront)
}
}
val transitionType = transitionType(remoteTransition)
val handler =
remoteTransition?.let {
OneShotRemoteHandler(transitions.mainExecutor, remoteTransition)
}
val transition = transitions.startTransition(transitionType, wct, handler)
handler?.setTransition(transition)
runOnTransitStart?.invoke(transition)
// Replaced by |IDesktopTaskListener#onActiveDeskChanged|.
if (!desktopState.enableMultipleDesktops) {
desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted(
toDesktopAnimationDurationMs
)
}
}
/**
* TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home.
*/
private fun prepareDeskDeactivationIfNeeded(
wct: WindowContainerTransaction,
deskId: Int?,
): RunOnTransitStart? {
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null
if (deskId == null) return null
desksOrganizer.deactivateDesk(wct, deskId)
return { transition ->
desksTransitionObserver.addPendingTransition(
DeskTransition.DeactivateDesk(
token = transition,
deskId = deskId,
runOnTransitEnd = { snapEventHandler.onDeskDeactivated(deskId) },
)
)
}
}
/** Removes the default desk in the given display. */
@Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()"))
fun removeDefaultDeskInDisplay(displayId: Int) {
val deskId = getOrCreateDefaultDeskId(displayId) ?: return
removeDesk(displayId = displayId, deskId = deskId)
}
/**
* Returns the default desk if it exists, or creates it if needed.
*
* Note: [DesktopDisplayEventHandler] is responsible for creating a default desk in
* desktop-first displays or warming up a desk-root in touch-first displays. This guarantees
* that a non-null desk can be returned by this function because even if one does not exist yet,
* [createDeskImmediate] should succeed.
*
* TODO: b/406890311 - replace callers with [getOrCreateDefaultDeskIdSuspending], which is safer
* because it does not depend on pre-creating desk roots.
*/
@Deprecated(
"Use getOrCreateDefaultDeskIdSuspending() instead",
ReplaceWith("getOrCreateDefaultDeskIdSuspending()"),
)
private fun getOrCreateDefaultDeskId(displayId: Int): Int? {
val existingDefaultDeskId = taskRepository.getDefaultDeskId(displayId)
if (existingDefaultDeskId != null) {
return existingDefaultDeskId
}
val immediateDeskId = createDeskImmediate(displayId, userId)
if (immediateDeskId == null) {
logE(
"Failed to create immediate desk in displayId=%s for userId=%s:\n%s",
displayId,
userId,
Throwable().stackTraceToString(),
)
}
return immediateDeskId
}
private suspend fun getOrCreateDefaultDeskIdSuspending(displayId: Int): Int =
taskRepository.getDefaultDeskId(displayId)
?: createDeskSuspending(displayId, userId, enforceDeskLimit = false)
/** Removes the given desk. */
fun removeDesk(deskId: Int, desktopRepository: DesktopRepository = taskRepository) {
if (!desktopRepository.getAllDeskIds().contains(deskId)) {
logW("Request to remove desk=%d but desk not found for user=%d", deskId, userId)
return
}
val displayId = desktopRepository.getDisplayForDesk(deskId)
removeDesk(displayId = displayId, deskId = deskId, desktopRepository = desktopRepository)
}
/** Removes all the available desks on all displays. */
fun removeAllDesks() {
taskRepository.getAllDeskIds().forEach { deskId -> removeDesk(deskId) }
}
private fun removeDesk(
displayId: Int,
deskId: Int,
desktopRepository: DesktopRepository = taskRepository,
) {
if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return
logV("removeDesk deskId=%d from displayId=%d", deskId, displayId)
val tasksToRemove =
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desktopRepository.getActiveTaskIdsInDesk(deskId)
} else {
// TODO: 362720497 - make sure minimized windows are also removed in WM
// and the repository.
desktopRepository.removeDesk(deskId)
}
val wct = WindowContainerTransaction()
tasksToRemove.forEach {
// TODO: b/404595635 - consider moving this block into [DesksOrganizer].
val task = shellTaskOrganizer.getRunningTaskInfo(it)
if (task != null) {
wct.removeTask(task.token)
} else {
recentTasksController?.removeBackgroundTask(it)
}
}
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksOrganizer.removeDesk(wct, deskId, userId)
}
if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return
val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null)
if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
desksTransitionObserver.addPendingTransition(
DeskTransition.RemoveDesk(
token = transition,
displayId = displayId,
deskId = deskId,
tasks = tasksToRemove,
onDeskRemovedListener = onDeskRemovedListener,
runOnTransitEnd = { snapEventHandler.onDeskRemoved(deskId) },
)
)
}
}
/** Enter split by using the focused desktop task in given `displayId`. */
fun enterSplit(displayId: Int, leftOrTop: Boolean) {
getFocusedDesktopTask(displayId)?.let { requestSplit(it, leftOrTop) }
}
private fun getFocusedDesktopTask(displayId: Int): RunningTaskInfo? {
if (
!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
!DesktopExperienceFlags.EXCLUDE_DESK_ROOTS_FROM_DESKTOP_TASKS.isTrue
) {
return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
}
}
val deskId = taskRepository.getActiveDeskId(displayId) ?: return null
return shellTaskOrganizer.getRunningTasks(displayId).find { taskInfo ->
taskInfo.isFocused && taskRepository.isActiveTaskInDesk(taskInfo.taskId, deskId)
}
}
/**
* Requests a task be transitioned from desktop to split select. Applies needed windowing
* changes if this transition is enabled.
*/
@JvmOverloads
fun requestSplit(taskInfo: RunningTaskInfo, leftOrTop: Boolean = false) {
// If a drag to desktop is in progress, we want to enter split select
// even if the requesting task is already in split.
val isDragging = dragToDesktopTransitionHandler.inProgress
val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging
if (shouldRequestSplit) {
if (isDragging) {
releaseVisualIndicator()
val cancelState =
if (leftOrTop) {
DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT
} else {
DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT
}
dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState)
} else {
val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
logV("Split requested for task=%d in desk=%d", taskInfo.taskId, deskId)
val wct = WindowContainerTransaction()
addMoveToSplitChanges(wct, taskInfo)
splitScreenController.requestEnterSplitSelect(
taskInfo,
if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
taskInfo.configuration.windowConfiguration.bounds,
/* startRecents = */ true,
/* withRecentsWct = */ wct,
)
}
}
}
/** Requests a task be transitioned from whatever mode it's in to a bubble. */
@JvmOverloads
fun requestFloat(taskInfo: RunningTaskInfo, left: Boolean? = null) {
val isDragging = dragToDesktopTransitionHandler.inProgress
val shouldRequestFloat =
taskInfo.isFullscreen || taskInfo.isFreeform || isDragging || taskInfo.isMultiWindow
if (!shouldRequestFloat) return
if (isDragging) {
releaseVisualIndicator()
val cancelState =
if (left == true) DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT
else DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT
dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState)
} else {
bubbleController.ifPresent {
it.expandStackAndSelectBubble(taskInfo, /* dragData= */ null)
}
}
}
private fun getDefaultDensityDpi(): Int = context.resources.displayMetrics.densityDpi
/** Creates a new instance of the external interface to pass to another process. */
private fun createExternalInterface(): ExternalInterfaceBinder = IDesktopModeImpl(this)
/** Get connection interface between sysui and shell */
fun asDesktopMode(): DesktopMode {
return desktopMode
}
/**
* Perform checks required on drag move. Create/release fullscreen indicator as needed.
* Different sources for x and y coordinates are used due to different needs for each: We want
* split transitions to be based on input coordinates but fullscreen transition to be based on
* task edge coordinate.
*
* @param taskInfo the task being dragged.
* @param taskSurface SurfaceControl of dragged task.
* @param displayId displayId of the input event.
* @param inputX x coordinate of input. Used for checks against left/right edge of screen.
* @param inputY y coordinate of input. Used for checks about cross display drag.
* @param taskBounds bounds of dragged task. Used for checks against status bar height.
*/
fun onDragPositioningMove(
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl,
displayId: Int,
inputX: Float,
inputY: Float,
taskBounds: Rect,
) {
if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return
snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId)
if (!DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG.isTrue()) {
updateVisualIndicator(
taskInfo,
taskSurface,
displayId,
inputX,
taskBounds.top.toFloat(),
DragStartState.FROM_FREEFORM,
)
return
}
val indicator =
getOrCreateVisualIndicator(taskInfo, taskSurface, DragStartState.FROM_FREEFORM)
val indicatorType =
indicator.calculateIndicatorType(displayId, PointF(inputX, taskBounds.top.toFloat()))
visualIndicatorUpdateScheduler.schedule(
taskInfo.displayId,
indicatorType,
inputX,
inputY,
taskBounds,
indicator,
)
}
fun updateVisualIndicator(
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl?,
displayId: Int,
inputX: Float,
taskTop: Float,
dragStartState: DragStartState,
): IndicatorType {
return getOrCreateVisualIndicator(taskInfo, taskSurface, dragStartState)
.updateIndicatorType(displayId, PointF(inputX, taskTop))
}
@VisibleForTesting
fun getOrCreateVisualIndicator(
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl?,
dragStartState: DragStartState,
): DesktopModeVisualIndicator {
// If the visual indicator has the wrong start state, it was never cleared from a previous
// drag event and needs to be cleared
if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) {
Slog.e(TAG, "Visual indicator from previous motion event was never released")
releaseVisualIndicator()
}
// If the visual indicator does not exist, create it.
val indicator =
visualIndicator
?: DesktopModeVisualIndicator(
desktopExecutor,
mainExecutor,
syncQueue,
taskInfo,
displayController,
if (ENABLE_BUG_FIXES_FOR_SECONDARY_DISPLAY.isTrue) {
displayController.getDisplayContext(taskInfo.displayId)
} else {
context
},
taskSurface,
rootTaskDisplayAreaOrganizer,
dragStartState,
bubbleController.getOrNull()?.bubbleDropTargetBoundsProvider,
snapEventHandler,
)
if (visualIndicator == null) visualIndicator = indicator
return indicator
}
/**
* Perform checks required on drag end. If indicator indicates a windowing mode change, perform
* that change. Otherwise, ensure bounds are up to date.
*
* @param taskInfo the task being dragged.
* @param taskSurface the leash of the task being dragged.
* @param displayId the displayId of the input event.
* @param inputCoordinate the coordinates of the motion event
* @param currentDragBounds the current bounds of where the visible task is (might be actual
* task bounds or just task leash)
* @param validDragArea the bounds of where the task can be dragged within the display.
* @param dragStartBounds the bounds of the task before starting dragging.
*/
fun onDragPositioningEnd(
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl,
displayId: Int,
inputCoordinate: PointF,
currentDragBounds: Rect,
validDragArea: Rect,
dragStartBounds: Rect,
motionEvent: MotionEvent,
) {
if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) {
return
}
val indicator = getVisualIndicator() ?: return
val indicatorType =
indicator.updateIndicatorType(
displayId,
PointF(inputCoordinate.x, currentDragBounds.top.toFloat()),
)
when (indicatorType) {
IndicatorType.TO_FULLSCREEN_INDICATOR -> {
val shouldMaximizeWhenDragToTopEdge =
if (DesktopExperienceFlags.ENABLE_DESKTOP_FIRST_BASED_DRAG_TO_MAXIMIZE.isTrue)
rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(
motionEvent.getDisplayId()
)
else desktopConfig.shouldMaximizeWhenDragToTopEdge
if (shouldMaximizeWhenDragToTopEdge) {
dragToMaximizeDesktopTask(taskInfo, taskSurface, currentDragBounds, motionEvent)
} else {
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HEADER_DRAG_TO_FULL_SCREEN,
)
moveToFullscreenWithAnimation(
taskInfo,
Point(currentDragBounds.left, currentDragBounds.top),
DesktopModeTransitionSource.TASK_DRAG,
)
}
}
IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HEADER_DRAG_TO_TILE_TO_LEFT,
)
handleSnapResizingTaskOnDrag(
taskInfo,
SnapPosition.LEFT,
taskSurface,
currentDragBounds,
dragStartBounds,
motionEvent,
)
}
IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HEADER_DRAG_TO_TILE_TO_RIGHT,
)
handleSnapResizingTaskOnDrag(
taskInfo,
SnapPosition.RIGHT,
taskSurface,
currentDragBounds,
dragStartBounds,
motionEvent,
)
}
IndicatorType.NO_INDICATOR,
IndicatorType.TO_BUBBLE_LEFT_INDICATOR,
IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> {
// TODO(b/391928049): add support fof dragging desktop apps to a bubble
// Create a copy so that we can animate from the current bounds if we end up having
// to snap the surface back without a WCT change.
val destinationBounds = Rect(currentDragBounds)
// If task bounds are outside valid drag area, snap them inward
DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(
destinationBounds,
validDragArea,
)
if (destinationBounds == dragStartBounds) {
// There's no actual difference between the start and end bounds, so while a
// WCT change isn't needed, the dragged surface still needs to be snapped back
// to its original location.
releaseVisualIndicator()
returnToDragStartAnimator.start(
taskInfo.taskId,
taskSurface,
startBounds = currentDragBounds,
endBounds = dragStartBounds,
)
return
}
val newDisplayId = motionEvent.getDisplayId()
val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(newDisplayId)
val isCrossDisplayDrag =
DesktopExperienceFlags.ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG.isTrue() &&
newDisplayId != taskInfo.getDisplayId() &&
displayAreaInfo != null
if (isCrossDisplayDrag) {
moveToDisplay(
taskInfo,
newDisplayId,
destinationBounds,
dragToDisplayTransitionHandler,
)
} else {
// Update task bounds so that the task position will match the position of its
// leash
val wct = WindowContainerTransaction()
wct.setBounds(taskInfo.token, destinationBounds)
transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
}
releaseVisualIndicator()
}
IndicatorType.TO_DESKTOP_INDICATOR -> {
throw IllegalArgumentException(
"Should not be receiving TO_DESKTOP_INDICATOR for " + "a freeform task."
)
}
}
// A freeform drag-move ended, remove the indicator immediately.
releaseVisualIndicator()
taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(
doesAnyTaskRequireTaskbarRounding(taskInfo.displayId)
)
}
/**
* Cancel the drag-to-desktop transition.
*
* @param taskInfo the task being dragged.
*/
fun onDragPositioningCancelThroughStatusBar(taskInfo: RunningTaskInfo) {
interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
cancelDragToDesktop(taskInfo)
}
/**
* Perform checks required when drag ends under status bar area.
*
* @param displayId the displayId of the input event.
* @param taskInfo the task being dragged.
* @param y height of drag, to be checked against status bar height.
* @return the [IndicatorType] used for the resulting transition
*/
fun onDragPositioningEndThroughStatusBar(
displayId: Int,
inputCoordinates: PointF,
taskInfo: RunningTaskInfo,
taskSurface: SurfaceControl,
): IndicatorType {
// End the drag_hold CUJ interaction.
interactionJankMonitor.end(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)
val indicator = getVisualIndicator() ?: return IndicatorType.NO_INDICATOR
val indicatorType = indicator.updateIndicatorType(displayId, inputCoordinates)
when (indicatorType) {
IndicatorType.TO_DESKTOP_INDICATOR -> {
latencyTracker.onActionStart(
LatencyTracker.ACTION_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG
)
// Start a new jank interaction for the drag release to desktop window animation.
interactionJankMonitor.begin(
taskSurface,
context,
handler,
CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE,
"to_desktop",
)
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_DESKTOP_MODE,
)
finalizeDragToDesktop(taskInfo)
}
IndicatorType.NO_INDICATOR,
IndicatorType.TO_FULLSCREEN_INDICATOR -> {
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_FULL_SCREEN,
)
cancelDragToDesktop(taskInfo)
}
IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_SPLIT_SCREEN,
)
requestSplit(taskInfo, leftOrTop = true)
}
IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
desktopModeUiEventLogger.log(
taskInfo,
DesktopUiEventEnum.DESKTOP_WINDOW_APP_HANDLE_DRAG_TO_SPLIT_SCREEN,
)
requestSplit(taskInfo, leftOrTop = false)
}
IndicatorType.TO_BUBBLE_LEFT_INDICATOR -> {
requestFloat(taskInfo, left = true)
}
IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> {
requestFloat(taskInfo, left = false)
}
}
return indicatorType
}
/** Update the exclusion region for a specified task */
fun onExclusionRegionChanged(taskId: Int, exclusionRegion: Region) {
taskRepository.updateTaskExclusionRegions(taskId, exclusionRegion)
}
/** Remove a previously tracked exclusion region for a specified task. */
fun removeExclusionRegionForTask(taskId: Int) {
taskRepository.removeExclusionRegion(taskId)
}
/**
* Adds a listener to find out about changes in the visibility of freeform tasks.
*
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) {
taskRepository.addVisibleTasksListener(listener, callbackExecutor)
}
/**
* Adds a listener to track changes to desktop task gesture exclusion regions
*
* @param listener the listener to add.
* @param callbackExecutor the executor to call the listener on.
*/
fun setTaskRegionListener(listener: Consumer<Region>, callbackExecutor: Executor) {
taskRepository.setExclusionRegionListener(listener, callbackExecutor)
}
// TODO(b/358114479): Move this implementation into a separate class.
override fun onUnhandledDrag(
launchIntent: PendingIntent,
@UserIdInt userId: Int,
dragEvent: DragEvent,
onFinishCallback: Consumer<Boolean>,
): Boolean {
val destinationDisplay = dragEvent.displayId
if (
!desktopState.isDesktopModeSupportedOnDisplay(destinationDisplay) ||
getFocusedNonDesktopTasks(destinationDisplay).isNotEmpty()
) {
// Destination display does not support desktop or has focused
// non-freeform task; ignore the drop.
return false
}
val launchComponent = getComponent(launchIntent)
if (!multiInstanceHelper.supportsMultiInstanceSplit(launchComponent, userId)) {
// TODO(b/320797628): Should only return early if there is an existing running task, and
// notify the user as well. But for now, just ignore the drop.
logV("Dropped intent does not support multi-instance")
return false
}
val taskInfo =
shellTaskOrganizer.getRunningTaskInfo(focusTransitionObserver.globallyFocusedTaskId)
?: return false
// TODO(b/358114479): Update drag and drop handling to give us visibility into when another
// window will accept a drag event. This way, we can hide the indicator when we won't
// be handling the transition here, allowing us to display the indicator accurately.
// For now, we create the indicator only on drag end and immediately dispose it.
val indicatorType =
updateVisualIndicator(
taskInfo,
dragEvent.dragSurface,
destinationDisplay,
dragEvent.x,
dragEvent.y,
DragStartState.DRAGGED_INTENT,
)
releaseVisualIndicator()
val windowingMode =
when (indicatorType) {
IndicatorType.TO_FULLSCREEN_INDICATOR -> {
WINDOWING_MODE_FULLSCREEN
}
// NO_INDICATOR can result from a cross-display drag.
IndicatorType.TO_SPLIT_LEFT_INDICATOR,
IndicatorType.TO_SPLIT_RIGHT_INDICATOR,
IndicatorType.TO_DESKTOP_INDICATOR,
IndicatorType.NO_INDICATOR -> {
WINDOWING_MODE_FREEFORM
}
else -> error("Invalid indicator type: $indicatorType")
}
val displayLayout = displayController.getDisplayLayout(destinationDisplay) ?: return false
val newWindowBounds = Rect()
when (indicatorType) {
IndicatorType.TO_DESKTOP_INDICATOR,
IndicatorType.NO_INDICATOR -> {
// Use default bounds, but with the top-center at the drop point.
newWindowBounds.set(calculateDefaultDesktopTaskBounds(displayLayout))
newWindowBounds.offsetTo(
dragEvent.x.toInt() - (newWindowBounds.width() / 2),
dragEvent.y.toInt(),
)
}
IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
newWindowBounds.set(getSnapBounds(destinationDisplay, SnapPosition.RIGHT))
}
IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
newWindowBounds.set(getSnapBounds(destinationDisplay, SnapPosition.LEFT))
}
else -> {
// Use empty bounds for the fullscreen case.
}
}
// Start a new transition to launch the app
val opts =
ActivityOptions.makeBasic().apply {
launchWindowingMode = windowingMode
launchBounds = newWindowBounds
pendingIntentBackgroundActivityStartMode =
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS
pendingIntentLaunchFlags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
launchDisplayId = destinationDisplay
}
if (windowingMode == WINDOWING_MODE_FULLSCREEN) {
dragAndDropFullscreenCookie = Binder()
opts.launchCookie = dragAndDropFullscreenCookie
}
val wct = WindowContainerTransaction()
wct.sendPendingIntent(launchIntent, null, opts.toBundle())
if (windowingMode == WINDOWING_MODE_FREEFORM) {
if (
DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue ||
DesktopExperienceFlags.ENABLE_DESKTOP_TAB_TEARING_LAUNCH_ANIMATION.isTrue
) {
val deskId = getOrCreateDefaultDeskId(destinationDisplay) ?: return false
startLaunchTransition(
TRANSIT_OPEN,
wct,
launchingTaskId = null,
deskId = deskId,
displayId = destinationDisplay,
dragEvent = dragEvent,
)
} else {
desktopModeDragAndDropTransitionHandler.handleDropEvent(wct, dragEvent)
}
} else {
transitions.startTransition(TRANSIT_OPEN, wct, null)
}
// Report that this is handled by the listener
onFinishCallback.accept(true)
// We've assumed responsibility of cleaning up the drag surface, so do that now
// TODO(b/320797628): Do an actual animation here for the drag surface
val t = SurfaceControl.Transaction()
t.remove(dragEvent.dragSurface)
t.apply()
return true
}
// TODO(b/366397912): Support full multi-user mode in Windowing.
override fun onUserChanged(newUserId: Int, userContext: Context) {
logV("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId)
updateCurrentUser(newUserId)
}
private fun updateCurrentUser(newUserId: Int) {
userId = newUserId
taskRepository = userRepositories.getProfile(userId)
if (this::snapEventHandler.isInitialized) {
snapEventHandler.onUserChange(userId)
}
}
/** Called when a task's info changes. */
fun onTaskInfoChanged(taskInfo: RunningTaskInfo) {
if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return
val inImmersive = taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)
val requestingImmersive = taskInfo.requestingImmersive
if (
inImmersive &&
!requestingImmersive &&
!RecentsTransitionStateListener.isRunning(recentsTransitionState)
) {
// Exit immersive if the app is no longer requesting it.
desktopImmersiveController.moveTaskToNonImmersive(
taskInfo,
DesktopImmersiveController.ExitReason.APP_NOT_IMMERSIVE,
)
}
}
private fun createActivityOptionsForStartTask(): ActivityOptions {
return ActivityOptions.makeBasic().apply {
launchWindowingMode = WINDOWING_MODE_FREEFORM
splashScreenStyle = SPLASH_SCREEN_STYLE_ICON
}
}
private fun dump(pw: PrintWriter, prefix: String) {
val innerPrefix = "$prefix "
pw.println("${prefix}DesktopTasksController")
desktopConfig.dump(pw, innerPrefix)
userRepositories.dump(pw, innerPrefix)
focusTransitionObserver.dump(pw, innerPrefix)
if (Flags.showDesktopExperienceDevOption()) {
dumpFlags(pw, prefix)
}
}
private fun dumpFlags(pw: PrintWriter, prefix: String) {
val flagPrefix = "$prefix "
fun dumpFlag(
name: String,
flagNameWidth: Int,
value: Boolean,
flagValue: Boolean,
overridable: Boolean,
) {
val spaces = " ".repeat(flagNameWidth - name.length)
pw.println(
"${flagPrefix}Flag $name$spaces - $value (default: $flagValue, overridable: $overridable)"
)
}
fun dumpFlag(flag: DesktopExperienceFlags, flagNameWidth: Int) {
dumpFlag(flag.flagName, flagNameWidth, flag.isTrue, flag.flagValue, flag.isOverridable)
}
fun dumpFlag(flag: DesktopExperienceFlag, flagNameWidth: Int) {
dumpFlag(flag.flagName, flagNameWidth, flag.isTrue, flag.flagValue, flag.isOverridable)
}
pw.println("${prefix}DesktopExperienceFlags")
pw.println(
"$prefix Status: ${if (DesktopExperienceFlags.getToggleOverride()) "enabled" else "disabled"}"
)
val maxEnumFlagName = DesktopExperienceFlags.entries.maxOf { it.flagName.length }
for (flag in DesktopExperienceFlags.entries) {
dumpFlag(flag, maxEnumFlagName + 1)
}
val registeredFlags = DesktopExperienceFlags.getRegisteredFlags()
val maxRegisteredFlagName = registeredFlags.maxOf { it.flagName.length }
pw.println("${prefix}DesktopExperienceFlags.DesktopExperienceFlag")
for (flag in registeredFlags) {
dumpFlag(flag, maxRegisteredFlagName + 1)
}
}
/** The interface for calls from outside the shell, within the host process. */
@ExternalThread
private inner class DesktopModeImpl : DesktopMode {
override fun addVisibleTasksListener(
listener: VisibleTasksListener,
callbackExecutor: Executor,
) {
mainExecutor.execute {
this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor)
}
}
override fun addDesktopGestureExclusionRegionListener(
listener: Consumer<Region>,
callbackExecutor: Executor,
) {
mainExecutor.execute {
this@DesktopTasksController.setTaskRegionListener(listener, callbackExecutor)
}
}
override fun moveFocusedTaskToDesktop(
displayId: Int,
transitionSource: DesktopModeTransitionSource,
) {
logV("moveFocusedTaskToDesktop")
mainExecutor.execute {
this@DesktopTasksController.moveFocusedTaskToDesktop(displayId, transitionSource)
}
}
override fun moveFocusedTaskToFullscreen(
displayId: Int,
transitionSource: DesktopModeTransitionSource,
) {
logV("moveFocusedTaskToFullscreen")
mainExecutor.execute {
this@DesktopTasksController.enterFullscreen(displayId, transitionSource)
}
}
override fun moveFocusedTaskToStageSplit(displayId: Int, leftOrTop: Boolean) {
logV("moveFocusedTaskToStageSplit")
mainExecutor.execute { this@DesktopTasksController.enterSplit(displayId, leftOrTop) }
}
override fun registerDesktopFirstListener(listener: DesktopFirstListener) {
logV("registerDesktopFirstListener")
if (desktopFirstListenerManager.isEmpty) {
throw UnsupportedOperationException(
"DesktopFirstListenerManager is not available on this device"
)
}
mainExecutor.execute { desktopFirstListenerManager.get().registerListener(listener) }
}
override fun unregisterDesktopFirstListener(listener: DesktopFirstListener) {
logV("unregisterDesktopFirstListener")
if (desktopFirstListenerManager.isEmpty) {
throw UnsupportedOperationException(
"DesktopFirstListenerManager is not available on this device"
)
}
mainExecutor.execute { desktopFirstListenerManager.get().unregisterListener(listener) }
}
}
/** The interface for calls from outside the host process. */
@BinderThread
private class IDesktopModeImpl(private var controller: DesktopTasksController?) :
IDesktopMode.Stub(), ExternalInterfaceBinder {
private lateinit var remoteListener:
SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>
private val deskChangeListener: DeskChangeListener =
object : DeskChangeListener {
override fun onDeskAdded(displayId: Int, deskId: Int) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onDeskAdded display=%d deskId=%d",
displayId,
deskId,
)
remoteListener.call { l -> l.onDeskAdded(displayId, deskId) }
}
override fun onDeskRemoved(displayId: Int, deskId: Int) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onDeskRemoved display=%d deskId=%d",
displayId,
deskId,
)
remoteListener.call { l -> l.onDeskRemoved(displayId, deskId) }
}
override fun onActiveDeskChanged(
displayId: Int,
newActiveDeskId: Int,
oldActiveDeskId: Int,
) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onActiveDeskChanged display=%d new=%d old=%d",
displayId,
newActiveDeskId,
oldActiveDeskId,
)
remoteListener.call { l ->
l.onActiveDeskChanged(displayId, newActiveDeskId, oldActiveDeskId)
}
}
override fun onCanCreateDesksChanged(canCreateDesks: Boolean) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onCanCreateDesksChanged canCreateDesks=%b",
canCreateDesks,
)
remoteListener.call { l -> l.onCanCreateDesksChanged(canCreateDesks) }
}
}
private val visibleTasksListener: VisibleTasksListener =
object : VisibleTasksListener {
override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onVisibilityChanged display=%d visible=%d",
displayId,
visibleTasksCount,
)
remoteListener.call { l ->
l.onTasksVisibilityChanged(displayId, visibleTasksCount)
}
}
}
private val taskbarDesktopTaskListener: TaskbarDesktopTaskListener =
object : TaskbarDesktopTaskListener {
override fun onTaskbarCornerRoundingUpdate(
hasTasksRequiringTaskbarRounding: Boolean
) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onTaskbarCornerRoundingUpdate " +
"doesAnyTaskRequireTaskbarRounding=%s",
hasTasksRequiringTaskbarRounding,
)
remoteListener.call { l ->
l.onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding)
}
}
}
private val desktopModeEntryExitTransitionListener: DesktopModeEntryExitTransitionListener =
object : DesktopModeEntryExitTransitionListener {
override fun onEnterDesktopModeTransitionStarted(transitionDuration: Int) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onEnterDesktopModeTransitionStarted transitionTime=%s",
transitionDuration,
)
remoteListener.call { l ->
l.onEnterDesktopModeTransitionStarted(transitionDuration)
}
}
override fun onExitDesktopModeTransitionStarted(
transitionDuration: Int,
shouldEndUpAtHome: Boolean,
) {
ProtoLog.v(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: onExitDesktopModeTransitionStarted transitionTime=%s shouldEndUpAtHome=%b",
transitionDuration,
shouldEndUpAtHome,
)
remoteListener.call { l ->
l.onExitDesktopModeTransitionStarted(transitionDuration, shouldEndUpAtHome)
}
}
}
init {
remoteListener =
SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>(
controller,
{ c ->
run {
syncInitialState(c)
registerListeners(c)
}
},
{ c -> run { unregisterListeners(c) } },
)
}
/** Invalidates this instance, preventing future calls from updating the controller. */
override fun invalidate() {
remoteListener.unregister()
controller = null
}
override fun createDesk(displayId: Int) {
executeRemoteCallWithTaskPermission(controller, "createDesk") { c ->
c.createDesk(displayId)
}
}
override fun removeDesk(deskId: Int) {
executeRemoteCallWithTaskPermission(controller, "removeDesk") { c ->
c.removeDesk(deskId)
}
}
override fun removeAllDesks() {
executeRemoteCallWithTaskPermission(controller, "removeAllDesks") { c ->
c.removeAllDesks()
}
}
override fun activateDesk(
deskId: Int,
remoteTransition: RemoteTransition?,
taskIdInFront: Int,
) {
executeRemoteCallWithTaskPermission(controller, "activateDesk") { c ->
c.activateDesk(
deskId,
remoteTransition,
if (taskIdInFront != INVALID_TASK_ID) taskIdInFront else null,
)
}
}
override fun showDesktopApps(
displayId: Int,
remoteTransition: RemoteTransition?,
taskIdInFront: Int,
) {
executeRemoteCallWithTaskPermission(controller, "showDesktopApps") { c ->
c.showDesktopApps(
displayId,
remoteTransition,
if (taskIdInFront != INVALID_TASK_ID) taskIdInFront else null,
)
}
}
override fun showDesktopApp(
taskId: Int,
remoteTransition: RemoteTransition?,
toFrontReason: DesktopTaskToFrontReason,
) {
executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c ->
c.moveTaskToFront(taskId, remoteTransition, toFrontReason.toUnminimizeReason())
}
}
override fun moveToFullscreen(
taskId: Int,
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition?,
) {
executeRemoteCallWithTaskPermission(controller, "moveToFullscreen") { c ->
c.moveToFullscreen(taskId, transitionSource, remoteTransition)
}
}
override fun stashDesktopApps(displayId: Int) {
ProtoLog.w(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: stashDesktopApps is deprecated")
}
override fun hideStashedDesktopApps(displayId: Int) {
ProtoLog.w(
WM_SHELL_DESKTOP_MODE,
"IDesktopModeImpl: hideStashedDesktopApps is deprecated",
)
}
override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) {
executeRemoteCallWithTaskPermission(controller, "onDesktopSplitSelectAnimComplete") { c
->
c.onDesktopSplitSelectAnimComplete(taskInfo)
}
}
override fun setTaskListener(listener: IDesktopTaskListener?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "IDesktopModeImpl: set task listener=%s", listener)
executeRemoteCallWithTaskPermission(controller, "setTaskListener") { _ ->
listener?.let { remoteListener.register(it) } ?: remoteListener.unregister()
}
}
override fun moveToDesktop(
taskId: Int,
transitionSource: DesktopModeTransitionSource,
remoteTransition: RemoteTransition?,
callback: IMoveToDesktopCallback?,
) {
executeRemoteCallWithTaskPermission(controller, "moveTaskToDesktop") { c ->
c.moveTaskToDefaultDeskAndActivate(
taskId,
transitionSource = transitionSource,
remoteTransition = remoteTransition,
callback = callback,
)
}
}
override fun removeDefaultDeskInDisplay(displayId: Int) {
executeRemoteCallWithTaskPermission(controller, "removeDefaultDeskInDisplay") { c ->
c.removeDefaultDeskInDisplay(displayId)
}
}
override fun moveToExternalDisplay(taskId: Int) {
executeRemoteCallWithTaskPermission(controller, "moveTaskToExternalDisplay") { c ->
c.moveToNextDisplay(taskId)
}
}
override fun startLaunchIntentTransition(intent: Intent, options: Bundle, displayId: Int) {
executeRemoteCallWithTaskPermission(controller, "startLaunchIntentTransition") { c ->
c.startLaunchIntentTransition(intent, options, displayId)
}
}
private fun syncInitialState(c: DesktopTasksController) {
remoteListener.call { l ->
l.onListenerConnected(
c.taskRepository.getDeskDisplayStateForRemote(),
c.canCreateDesks(),
)
}
}
private fun registerListeners(c: DesktopTasksController) {
if (c.desktopState.enableMultipleDesktops) {
c.taskRepository.addDeskChangeListener(deskChangeListener, c.mainExecutor)
}
c.taskRepository.addVisibleTasksListener(visibleTasksListener, c.mainExecutor)
c.taskbarDesktopTaskListener = taskbarDesktopTaskListener
c.desktopModeEnterExitTransitionListener = desktopModeEntryExitTransitionListener
}
private fun unregisterListeners(c: DesktopTasksController) {
if (c.desktopState.enableMultipleDesktops) {
c.taskRepository.removeDeskChangeListener(deskChangeListener)
}
c.taskRepository.removeVisibleTasksListener(visibleTasksListener)
c.taskbarDesktopTaskListener = null
c.desktopModeEnterExitTransitionListener = null
}
}
private fun logV(msg: String, vararg arguments: Any?) {
ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
private fun logD(msg: String, vararg arguments: Any?) {
ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
private fun logW(msg: String, vararg arguments: Any?) {
ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
private fun logE(msg: String, vararg arguments: Any?) {
ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
}
companion object {
@JvmField
val DESKTOP_MODE_INITIAL_BOUNDS_SCALE =
SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f
// Timeout used for CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD, this is longer than the
// default timeout to avoid timing out in the middle of a drag action.
private val APP_HANDLE_DRAG_HOLD_CUJ_TIMEOUT_MS: Long = TimeUnit.SECONDS.toMillis(10L)
private const val TAG = "DesktopTasksController"
private fun DesktopTaskToFrontReason.toUnminimizeReason(): UnminimizeReason =
when (this) {
DesktopTaskToFrontReason.UNKNOWN -> UnminimizeReason.UNKNOWN
DesktopTaskToFrontReason.TASKBAR_TAP -> UnminimizeReason.TASKBAR_TAP
DesktopTaskToFrontReason.ALT_TAB -> UnminimizeReason.ALT_TAB
DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW ->
UnminimizeReason.TASKBAR_MANAGE_WINDOW
}
@JvmField
/**
* A placeholder for a synthetic transition that isn't backed by a true system transition.
*/
val SYNTHETIC_TRANSITION: IBinder = Binder()
private val enableAltTabKqsFlatenning: DesktopExperienceFlag =
DesktopExperienceFlag(
com.android.launcher3.Flags::enableAltTabKqsFlatenning,
/* shouldOverrideByDevOption= */ true,
com.android.launcher3.Flags.FLAG_ENABLE_ALT_TAB_KQS_FLATENNING,
)
}
/** Defines interface for classes that can listen to changes for task resize. */
// TODO(b/343931111): Migrate to using TransitionObservers when ready
interface TaskbarDesktopTaskListener {
/**
* [hasTasksRequiringTaskbarRounding] is true when a task is either maximized or snapped
* left/right and rounded corners are enabled.
*/
fun onTaskbarCornerRoundingUpdate(hasTasksRequiringTaskbarRounding: Boolean)
}
/** Defines interface for entering and exiting desktop windowing mode. */
interface DesktopModeEntryExitTransitionListener {
/** [transitionDuration] time it takes to run enter desktop mode transition */
fun onEnterDesktopModeTransitionStarted(transitionDuration: Int)
/** [transitionDuration] time it takes to run exit desktop mode transition */
fun onExitDesktopModeTransitionStarted(transitionDuration: Int, shouldEndUpAtHome: Boolean)
}
/** The positions on a screen that a task can snap to. */
enum class SnapPosition {
RIGHT,
LEFT,
}
}