Loading libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +20 −2 Original line number Diff line number Diff line Loading @@ -65,6 +65,7 @@ import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; Loading Loading @@ -384,9 +385,11 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, Transitions transitions, Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler, WindowDecorViewModel windowDecorViewModel) { return new FreeformTaskTransitionObserver( context, shellInit, transitions, windowDecorViewModel); context, shellInit, transitions, desktopImmersiveTransitionHandler, windowDecorViewModel); } @WMSingleton Loading Loading @@ -621,6 +624,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopRepository desktopRepository, Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler, DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, Loading @@ -636,7 +640,8 @@ public abstract class WMShellModule { returnToDragStartAnimator, enterDesktopTransitionHandler, exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopRepository, dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(), desktopRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null), interactionJankMonitor, mainHandler); Loading Loading @@ -669,6 +674,19 @@ public abstract class WMShellModule { ); } @WMSingleton @Provides static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler( Context context, Transitions transitions, @DynamicOverride DesktopRepository desktopRepository) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of( new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository)); } return Optional.empty(); } @WMSingleton @Provides static ReturnToDragStartAnimator provideReturnToDragStartAnimator( Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt 0 → 100644 +245 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.desktopmode import android.animation.RectEvaluator import android.animation.ValueAnimator import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.animation.DecelerateInterpolator import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.core.animation.addListener import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener /** * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task * remains freeform while being able to take fullscreen bounds and have its App Header visibility * be transient below the status bar like in fullscreen immersive mode. */ class DesktopFullImmersiveTransitionHandler( private val transitions: Transitions, private val desktopRepository: DesktopRepository, private val transactionSupplier: () -> SurfaceControl.Transaction, ) : TransitionHandler { constructor( transitions: Transitions, desktopRepository: DesktopRepository, ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() }) private var state: TransitionState? = null /** Whether there is an immersive transition that hasn't completed yet. */ private val inProgress: Boolean get() = state != null private val rectEvaluator = RectEvaluator() /** A listener to invoke on animation changes during entry/exit. */ var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null /** Starts a transition to enter full immersive state inside the desktop. */ fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) { if (inProgress) { ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "FullImmersive: cannot start entry because transition already in progress." ) return } val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this) state = TransitionState( transition = transition, displayId = taskInfo.displayId, taskId = taskInfo.taskId, direction = Direction.ENTER ) } fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) { if (inProgress) { ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "$TAG: cannot start exit because transition already in progress." ) return } val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this) state = TransitionState( transition = transition, displayId = taskInfo.displayId, taskId = taskInfo.taskId, direction = Direction.EXIT ) } override fun startAnimation( transition: IBinder, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { val state = requireState() if (transition != state.transition) return false animateResize( transitionState = state, info = info, startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = finishCallback ) return true } private fun animateResize( transitionState: TransitionState, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ) { val change = info.changes.first { c -> val taskInfo = c.taskInfo return@first taskInfo != null && taskInfo.taskId == transitionState.taskId } val leash = change.leash val startBounds = change.startAbsBounds val endBounds = change.endAbsBounds val updateTransaction = transactionSupplier() ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply { duration = FULL_IMMERSIVE_ANIM_DURATION_MS interpolator = DecelerateInterpolator() addListener( onStart = { startTransaction .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat()) .setWindowCrop(leash, startBounds.width(), startBounds.height()) .show(leash) onTaskResizeAnimationListener ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds) ?: startTransaction.apply() }, onEnd = { finishTransaction .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat()) .setWindowCrop(leash, endBounds.width(), endBounds.height()) .apply() onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId) finishCallback.onTransitionFinished(null /* wct */) clearState() } ) addUpdateListener { animation -> val rect = animation.animatedValue as Rect updateTransaction .setPosition(leash, rect.left.toFloat(), rect.top.toFloat()) .setWindowCrop(leash, rect.width(), rect.height()) .apply() onTaskResizeAnimationListener ?.onBoundsChange(transitionState.taskId, updateTransaction, rect) ?: updateTransaction.apply() } start() } } override fun handleRequest( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? = null override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, finishTransaction: SurfaceControl.Transaction? ) { val state = this.state ?: return if (transition == state.transition && aborted) { clearState() } super.onTransitionConsumed(transition, aborted, finishTransaction) } /** * Called when any transition in the system is ready to play. This is needed to update the * repository state before window decorations are drawn (which happens immediately after * |onTransitionReady|, before this transition actually animates) because drawing decorations * depends in whether the task is in full immersive state or not. */ fun onTransitionReady(transition: IBinder) { val state = this.state ?: return // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit // immersive, which isn't realistic. The app could crash, the user could dismiss it from // overview, etc. This (or its caller) should search all transitions to look for any // immersive task exiting that state to keep the repository properly updated. if (transition == state.transition) { when (state.direction) { Direction.ENTER -> { desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = true ) } Direction.EXIT -> { desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = false ) } } } } private fun clearState() { state = null } private fun requireState(): TransitionState = state ?: error("Expected non-null transition state") /** The state of the currently running transition. */ private data class TransitionState( val transition: IBinder, val displayId: Int, val taskId: Int, val direction: Direction ) private enum class Direction { ENTER, EXIT } private companion object { private const val TAG = "FullImmersiveHandler" private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L } } libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +46 −12 Original line number Diff line number Diff line Loading @@ -134,6 +134,7 @@ class DesktopTasksController( private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler, private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler, private val taskRepository: DesktopRepository, private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, private val launchAdjacentController: LaunchAdjacentController, Loading Loading @@ -231,6 +232,7 @@ class DesktopTasksController( toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener immersiveTransitionHandler.onTaskResizeAnimationListener = listener } fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { Loading Loading @@ -649,6 +651,35 @@ class DesktopTasksController( } } /** Moves a task in/out of full immersive state within the desktop. */ fun toggleDesktopTaskFullImmersiveState(taskInfo: RunningTaskInfo) { if (taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)) { exitDesktopTaskFromFullImmersive(taskInfo) } else { moveDesktopTaskToFullImmersive(taskInfo) } } private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) { check(taskInfo.isFreeform) { "Task must already be in freeform" } val wct = WindowContainerTransaction().apply { setBounds(taskInfo.token, Rect()) } immersiveTransitionHandler.enterImmersive(taskInfo, wct) } private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) { check(taskInfo.isFreeform) { "Task must already be in freeform" } val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val destinationBounds = getMaximizeBounds(taskInfo, stableBounds) val wct = WindowContainerTransaction().apply { setBounds(taskInfo.token, destinationBounds) } immersiveTransitionHandler.exitImmersive(taskInfo, wct) } /** * 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 Loading Loading @@ -685,18 +716,7 @@ class DesktopTasksController( // and toggle to the stable bounds. taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds) if (taskInfo.isResizeable) { // if resizable then expand to entire stable bounds (full display minus insets) destinationBounds.set(stableBounds) } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) val newSize = maximizeSizeGivenAspectRatio(taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) val newBounds = centerInArea( newSize, stableBounds, stableBounds.left, stableBounds.top) destinationBounds.set(newBounds) } destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds)) } Loading @@ -719,6 +739,20 @@ class DesktopTasksController( } } private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect { if (taskInfo.isResizeable) { // if resizable then expand to entire stable bounds (full display minus insets) return Rect(stableBounds) } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) val newSize = maximizeSizeGivenAspectRatio(taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) return centerInArea( newSize, stableBounds, stableBounds.left, stableBounds.top) } } private fun isTaskMaximized( taskInfo: RunningTaskInfo, stableBounds: Rect Loading libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +13 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import android.window.WindowContainerToken; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.window.flags.Flags; import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; Loading @@ -36,6 +38,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; /** * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes, Loading @@ -44,6 +47,7 @@ import java.util.Map; */ public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver { private final Transitions mTransitions; private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler; private final WindowDecorViewModel mWindowDecorViewModel; private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo = Loading @@ -53,8 +57,10 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs Context context, ShellInit shellInit, Transitions transitions, Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler, WindowDecorViewModel windowDecorViewModel) { mTransitions = transitions; mImmersiveTransitionHandler = immersiveTransitionHandler; mWindowDecorViewModel = windowDecorViewModel; if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) { shellInit.addInitCallback(this::onInit, this); Loading @@ -72,6 +78,13 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT) { if (Flags.enableFullyImmersiveInDesktop()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository // is updated from there **before** the |mWindowDecorViewModel| methods are invoked. // Otherwise window decoration relayout won't run with the immersive state up to date. mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition)); } final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>(); final ArrayList<WindowContainerToken> taskParents = new ArrayList<>(); for (TransitionInfo.Change change : info.getChanges()) { Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +35 −7 Original line number Diff line number Diff line Loading @@ -538,6 +538,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.closeMaximizeMenu(); } private void onEnterOrExitImmersive(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { return; } mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo); } private void onSnapResize(int taskId, boolean left) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { Loading Loading @@ -755,7 +763,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // back to the decoration using // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which // should shared with the maximize menu's maximize/restore actions. if (Flags.enableFullyImmersiveInDesktop() && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) { // Task is requesting immersive, so it should either enter or exit immersive, // depending on immersive state. onEnterOrExitImmersive(decoration.mTaskInfo.taskId); } else { // Full immersive is disabled or task doesn't request/support it, so just // toggle between maximize/restore states. onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button"); } } else if (id == R.id.minimize_window) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId); Loading Loading @@ -935,14 +952,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window || id == R.id.open_menu_button || id == R.id.minimize_window); final boolean dragAllowed = !mDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { if (dragAllowed) { mDragPointerId = e.getPointerId(0); final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); updateDragStatus(e.getActionMasked()); mOnDragStartInitialBounds.set(initialBounds); } mHasLongClicked = false; // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. Loading @@ -951,6 +972,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { case ACTION_MOVE: { // If a decor's resize drag zone is active, don't also try to reposition it. if (decoration.isHandlingDragResize()) break; // Dragging the header isn't allowed, so skip the positioning work. if (!dragAllowed) break; decoration.closeMaximizeMenu(); if (e.findPointerIndex(mDragPointerId) == -1) { mDragPointerId = e.getPointerId(0); Loading Loading @@ -1036,6 +1060,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } if (mDesktopRepository.isTaskInFullImmersiveState(mTaskId)) { // Disallow double-tap to resize when in full immersive. return false; } onMaximizeOrRestore(mTaskId, "double_tap"); return true; } Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +20 −2 Original line number Diff line number Diff line Loading @@ -65,6 +65,7 @@ import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; Loading Loading @@ -384,9 +385,11 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, Transitions transitions, Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler, WindowDecorViewModel windowDecorViewModel) { return new FreeformTaskTransitionObserver( context, shellInit, transitions, windowDecorViewModel); context, shellInit, transitions, desktopImmersiveTransitionHandler, windowDecorViewModel); } @WMSingleton Loading Loading @@ -621,6 +624,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopRepository desktopRepository, Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler, DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, Loading @@ -636,7 +640,8 @@ public abstract class WMShellModule { returnToDragStartAnimator, enterDesktopTransitionHandler, exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopRepository, dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(), desktopRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter, recentTasksController.orElse(null), interactionJankMonitor, mainHandler); Loading Loading @@ -669,6 +674,19 @@ public abstract class WMShellModule { ); } @WMSingleton @Provides static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler( Context context, Transitions transitions, @DynamicOverride DesktopRepository desktopRepository) { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of( new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository)); } return Optional.empty(); } @WMSingleton @Provides static ReturnToDragStartAnimator provideReturnToDragStartAnimator( Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt 0 → 100644 +245 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wm.shell.desktopmode import android.animation.RectEvaluator import android.animation.ValueAnimator import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.animation.DecelerateInterpolator import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.core.animation.addListener import com.android.internal.protolog.ProtoLog import com.android.wm.shell.protolog.ShellProtoLogGroup import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener /** * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task * remains freeform while being able to take fullscreen bounds and have its App Header visibility * be transient below the status bar like in fullscreen immersive mode. */ class DesktopFullImmersiveTransitionHandler( private val transitions: Transitions, private val desktopRepository: DesktopRepository, private val transactionSupplier: () -> SurfaceControl.Transaction, ) : TransitionHandler { constructor( transitions: Transitions, desktopRepository: DesktopRepository, ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() }) private var state: TransitionState? = null /** Whether there is an immersive transition that hasn't completed yet. */ private val inProgress: Boolean get() = state != null private val rectEvaluator = RectEvaluator() /** A listener to invoke on animation changes during entry/exit. */ var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null /** Starts a transition to enter full immersive state inside the desktop. */ fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) { if (inProgress) { ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "FullImmersive: cannot start entry because transition already in progress." ) return } val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this) state = TransitionState( transition = transition, displayId = taskInfo.displayId, taskId = taskInfo.taskId, direction = Direction.ENTER ) } fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) { if (inProgress) { ProtoLog.v( ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "$TAG: cannot start exit because transition already in progress." ) return } val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this) state = TransitionState( transition = transition, displayId = taskInfo.displayId, taskId = taskInfo.taskId, direction = Direction.EXIT ) } override fun startAnimation( transition: IBinder, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { val state = requireState() if (transition != state.transition) return false animateResize( transitionState = state, info = info, startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = finishCallback ) return true } private fun animateResize( transitionState: TransitionState, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ) { val change = info.changes.first { c -> val taskInfo = c.taskInfo return@first taskInfo != null && taskInfo.taskId == transitionState.taskId } val leash = change.leash val startBounds = change.startAbsBounds val endBounds = change.endAbsBounds val updateTransaction = transactionSupplier() ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply { duration = FULL_IMMERSIVE_ANIM_DURATION_MS interpolator = DecelerateInterpolator() addListener( onStart = { startTransaction .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat()) .setWindowCrop(leash, startBounds.width(), startBounds.height()) .show(leash) onTaskResizeAnimationListener ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds) ?: startTransaction.apply() }, onEnd = { finishTransaction .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat()) .setWindowCrop(leash, endBounds.width(), endBounds.height()) .apply() onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId) finishCallback.onTransitionFinished(null /* wct */) clearState() } ) addUpdateListener { animation -> val rect = animation.animatedValue as Rect updateTransaction .setPosition(leash, rect.left.toFloat(), rect.top.toFloat()) .setWindowCrop(leash, rect.width(), rect.height()) .apply() onTaskResizeAnimationListener ?.onBoundsChange(transitionState.taskId, updateTransaction, rect) ?: updateTransaction.apply() } start() } } override fun handleRequest( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? = null override fun onTransitionConsumed( transition: IBinder, aborted: Boolean, finishTransaction: SurfaceControl.Transaction? ) { val state = this.state ?: return if (transition == state.transition && aborted) { clearState() } super.onTransitionConsumed(transition, aborted, finishTransaction) } /** * Called when any transition in the system is ready to play. This is needed to update the * repository state before window decorations are drawn (which happens immediately after * |onTransitionReady|, before this transition actually animates) because drawing decorations * depends in whether the task is in full immersive state or not. */ fun onTransitionReady(transition: IBinder) { val state = this.state ?: return // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit // immersive, which isn't realistic. The app could crash, the user could dismiss it from // overview, etc. This (or its caller) should search all transitions to look for any // immersive task exiting that state to keep the repository properly updated. if (transition == state.transition) { when (state.direction) { Direction.ENTER -> { desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = true ) } Direction.EXIT -> { desktopRepository.setTaskInFullImmersiveState( displayId = state.displayId, taskId = state.taskId, immersive = false ) } } } } private fun clearState() { state = null } private fun requireState(): TransitionState = state ?: error("Expected non-null transition state") /** The state of the currently running transition. */ private data class TransitionState( val transition: IBinder, val displayId: Int, val taskId: Int, val direction: Direction ) private enum class Direction { ENTER, EXIT } private companion object { private const val TAG = "FullImmersiveHandler" private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L } }
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +46 −12 Original line number Diff line number Diff line Loading @@ -134,6 +134,7 @@ class DesktopTasksController( private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler, private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler, private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler, private val taskRepository: DesktopRepository, private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver, private val launchAdjacentController: LaunchAdjacentController, Loading Loading @@ -231,6 +232,7 @@ class DesktopTasksController( toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener immersiveTransitionHandler.onTaskResizeAnimationListener = listener } fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { Loading Loading @@ -649,6 +651,35 @@ class DesktopTasksController( } } /** Moves a task in/out of full immersive state within the desktop. */ fun toggleDesktopTaskFullImmersiveState(taskInfo: RunningTaskInfo) { if (taskRepository.isTaskInFullImmersiveState(taskInfo.taskId)) { exitDesktopTaskFromFullImmersive(taskInfo) } else { moveDesktopTaskToFullImmersive(taskInfo) } } private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) { check(taskInfo.isFreeform) { "Task must already be in freeform" } val wct = WindowContainerTransaction().apply { setBounds(taskInfo.token, Rect()) } immersiveTransitionHandler.enterImmersive(taskInfo, wct) } private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) { check(taskInfo.isFreeform) { "Task must already be in freeform" } val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val destinationBounds = getMaximizeBounds(taskInfo, stableBounds) val wct = WindowContainerTransaction().apply { setBounds(taskInfo.token, destinationBounds) } immersiveTransitionHandler.exitImmersive(taskInfo, wct) } /** * 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 Loading Loading @@ -685,18 +716,7 @@ class DesktopTasksController( // and toggle to the stable bounds. taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds) if (taskInfo.isResizeable) { // if resizable then expand to entire stable bounds (full display minus insets) destinationBounds.set(stableBounds) } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) val newSize = maximizeSizeGivenAspectRatio(taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) val newBounds = centerInArea( newSize, stableBounds, stableBounds.left, stableBounds.top) destinationBounds.set(newBounds) } destinationBounds.set(getMaximizeBounds(taskInfo, stableBounds)) } Loading @@ -719,6 +739,20 @@ class DesktopTasksController( } } private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect { if (taskInfo.isResizeable) { // if resizable then expand to entire stable bounds (full display minus insets) return Rect(stableBounds) } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) val newSize = maximizeSizeGivenAspectRatio(taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio) return centerInArea( newSize, stableBounds, stableBounds.left, stableBounds.top) } } private fun isTaskMaximized( taskInfo: RunningTaskInfo, stableBounds: Rect Loading
libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +13 −0 Original line number Diff line number Diff line Loading @@ -27,6 +27,8 @@ import android.window.WindowContainerToken; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.window.flags.Flags; import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.WindowDecorViewModel; Loading @@ -36,6 +38,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; /** * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes, Loading @@ -44,6 +47,7 @@ import java.util.Map; */ public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver { private final Transitions mTransitions; private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler; private final WindowDecorViewModel mWindowDecorViewModel; private final Map<IBinder, List<ActivityManager.RunningTaskInfo>> mTransitionToTaskInfo = Loading @@ -53,8 +57,10 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs Context context, ShellInit shellInit, Transitions transitions, Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler, WindowDecorViewModel windowDecorViewModel) { mTransitions = transitions; mImmersiveTransitionHandler = immersiveTransitionHandler; mWindowDecorViewModel = windowDecorViewModel; if (Transitions.ENABLE_SHELL_TRANSITIONS && FreeformComponents.isFreeformEnabled(context)) { shellInit.addInitCallback(this::onInit, this); Loading @@ -72,6 +78,13 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT) { if (Flags.enableFullyImmersiveInDesktop()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository // is updated from there **before** the |mWindowDecorViewModel| methods are invoked. // Otherwise window decoration relayout won't run with the immersive state up to date. mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition)); } final ArrayList<ActivityManager.RunningTaskInfo> taskInfoList = new ArrayList<>(); final ArrayList<WindowContainerToken> taskParents = new ArrayList<>(); for (TransitionInfo.Change change : info.getChanges()) { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +35 −7 Original line number Diff line number Diff line Loading @@ -538,6 +538,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.closeMaximizeMenu(); } private void onEnterOrExitImmersive(int taskId) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { return; } mDesktopTasksController.toggleDesktopTaskFullImmersiveState(decoration.mTaskInfo); } private void onSnapResize(int taskId, boolean left) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskId); if (decoration == null) { Loading Loading @@ -755,7 +763,16 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // back to the decoration using // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which // should shared with the maximize menu's maximize/restore actions. if (Flags.enableFullyImmersiveInDesktop() && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) { // Task is requesting immersive, so it should either enter or exit immersive, // depending on immersive state. onEnterOrExitImmersive(decoration.mTaskInfo.taskId); } else { // Full immersive is disabled or task doesn't request/support it, so just // toggle between maximize/restore states. onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button"); } } else if (id == R.id.minimize_window) { final WindowContainerTransaction wct = new WindowContainerTransaction(); mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId); Loading Loading @@ -935,14 +952,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } final boolean touchingButton = (id == R.id.close_window || id == R.id.maximize_window || id == R.id.open_menu_button || id == R.id.minimize_window); final boolean dragAllowed = !mDesktopRepository.isTaskInFullImmersiveState(taskInfo.taskId); switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: { if (dragAllowed) { mDragPointerId = e.getPointerId(0); final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart( 0 /* ctrlType */, e.getRawX(0), e.getRawY(0)); updateDragStatus(e.getActionMasked()); mOnDragStartInitialBounds.set(initialBounds); } mHasLongClicked = false; // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. Loading @@ -951,6 +972,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { case ACTION_MOVE: { // If a decor's resize drag zone is active, don't also try to reposition it. if (decoration.isHandlingDragResize()) break; // Dragging the header isn't allowed, so skip the positioning work. if (!dragAllowed) break; decoration.closeMaximizeMenu(); if (e.findPointerIndex(mDragPointerId) == -1) { mDragPointerId = e.getPointerId(0); Loading Loading @@ -1036,6 +1060,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } if (mDesktopRepository.isTaskInFullImmersiveState(mTaskId)) { // Disallow double-tap to resize when in full immersive. return false; } onMaximizeOrRestore(mTaskId, "double_tap"); return true; } Loading