5261 lines
223 KiB
Kotlin
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,
|
|
}
|
|
}
|