Merge changes from topic "desktoo-immersive-handler" into main
* changes:
Add DesktopFullImmersiveTransitionHandler
Disable App Header drag-move and drag-resize in full immersive
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 0dca97c..b7d9677 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -65,6 +65,7 @@
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;
@@ -384,9 +385,11 @@
Context context,
ShellInit shellInit,
Transitions transitions,
+ Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
WindowDecorViewModel windowDecorViewModel) {
return new FreeformTaskTransitionObserver(
- context, shellInit, transitions, windowDecorViewModel);
+ context, shellInit, transitions, desktopImmersiveTransitionHandler,
+ windowDecorViewModel);
}
@WMSingleton
@@ -621,6 +624,7 @@
ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
@DynamicOverride DesktopRepository desktopRepository,
+ Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler,
DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
LaunchAdjacentController launchAdjacentController,
RecentsTransitionHandler recentsTransitionHandler,
@@ -636,7 +640,8 @@
returnToDragStartAnimator, enterDesktopTransitionHandler,
exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler,
toggleResizeDesktopTaskTransitionHandler,
- dragToDesktopTransitionHandler, desktopRepository,
+ dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(),
+ desktopRepository,
desktopModeLoggerTransitionObserver, launchAdjacentController,
recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
recentTasksController.orElse(null), interactionJankMonitor, mainHandler);
@@ -671,6 +676,19 @@
@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(
Context context, InteractionJankMonitor interactionJankMonitor) {
return new ReturnToDragStartAnimator(context, interactionJankMonitor);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
new file mode 100644
index 0000000..f749aa1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
@@ -0,0 +1,245 @@
+/*
+ * 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
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 2303e71..4e548a6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -134,6 +134,7 @@
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,
@@ -231,6 +232,7 @@
toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
+ immersiveTransitionHandler.onTaskResizeAnimationListener = listener
}
fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -649,6 +651,35 @@
}
}
+ /** 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
@@ -685,18 +716,7 @@
// 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))
}
@@ -719,6 +739,20 @@
}
}
+ 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
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index ffcc526..cb0354ec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -27,6 +27,8 @@
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;
@@ -36,6 +38,7 @@
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,
@@ -44,6 +47,7 @@
*/
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 =
@@ -53,8 +57,10 @@
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);
@@ -72,6 +78,13 @@
@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()) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index bf175b7..bcf48d9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -538,6 +538,14 @@
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) {
@@ -755,7 +763,16 @@
// back to the decoration using
// {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which
// should shared with the maximize menu's maximize/restore actions.
- onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
+ 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);
@@ -935,14 +952,18 @@
}
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: {
- mDragPointerId = e.getPointerId(0);
- final Rect initialBounds = mDragPositioningCallback.onDragPositioningStart(
- 0 /* ctrlType */, e.getRawX(0),
- e.getRawY(0));
- updateDragStatus(e.getActionMasked());
- mOnDragStartInitialBounds.set(initialBounds);
+ 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.
@@ -951,6 +972,9 @@
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);
@@ -1036,6 +1060,10 @@
&& 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;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index c7e8422..25d37fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -517,8 +517,8 @@
closeManageWindowsMenu();
closeMaximizeMenu();
}
- updateDragResizeListener(oldDecorationSurface);
- updateMaximizeMenu(startT);
+ updateDragResizeListener(oldDecorationSurface, inFullImmersive);
+ updateMaximizeMenu(startT, inFullImmersive);
Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces
}
@@ -571,11 +571,12 @@
return mUserContext.getUser();
}
- private void updateDragResizeListener(SurfaceControl oldDecorationSurface) {
- if (!isDragResizable(mTaskInfo)) {
+ private void updateDragResizeListener(SurfaceControl oldDecorationSurface,
+ boolean inFullImmersive) {
+ if (!isDragResizable(mTaskInfo, inFullImmersive)) {
if (!mTaskInfo.positionInParent.equals(mPositionInParent)) {
// We still want to track caption bar's exclusion region on a non-resizeable task.
- updateExclusionRegion();
+ updateExclusionRegion(inFullImmersive);
}
closeDragResizeListener();
return;
@@ -609,11 +610,16 @@
getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res),
getFineResizeCornerSize(res), getLargeResizeCornerSize(res)), touchSlop)
|| !mTaskInfo.positionInParent.equals(mPositionInParent)) {
- updateExclusionRegion();
+ updateExclusionRegion(inFullImmersive);
}
}
- private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) {
+ private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo,
+ boolean inFullImmersive) {
+ if (inFullImmersive) {
+ // Task cannot be resized in full immersive.
+ return false;
+ }
if (DesktopModeFlags.ENABLE_WINDOWING_SCALED_RESIZING.isTrue()) {
return taskInfo.isFreeform();
}
@@ -677,8 +683,8 @@
mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState);
}
- private void updateMaximizeMenu(SurfaceControl.Transaction startT) {
- if (!isDragResizable(mTaskInfo) || !isMaximizeMenuActive()) {
+ private void updateMaximizeMenu(SurfaceControl.Transaction startT, boolean inFullImmersive) {
+ if (!isDragResizable(mTaskInfo, inFullImmersive) || !isMaximizeMenuActive()) {
return;
}
if (!mTaskInfo.isVisible()) {
@@ -1546,24 +1552,29 @@
mPositionInParent.set(mTaskInfo.positionInParent);
}
- private void updateExclusionRegion() {
+ private void updateExclusionRegion(boolean inFullImmersive) {
// An outdated position in parent is one reason for this to be called; update it here.
updatePositionInParent();
mExclusionRegionListener
- .onExclusionRegionChanged(mTaskInfo.taskId, getGlobalExclusionRegion());
+ .onExclusionRegionChanged(mTaskInfo.taskId,
+ getGlobalExclusionRegion(inFullImmersive));
}
/**
* Create a new exclusion region from the corner rects (if resizeable) and caption bounds
* of this task.
*/
- private Region getGlobalExclusionRegion() {
+ private Region getGlobalExclusionRegion(boolean inFullImmersive) {
Region exclusionRegion;
- if (mDragResizeListener != null && isDragResizable(mTaskInfo)) {
+ if (mDragResizeListener != null && isDragResizable(mTaskInfo, inFullImmersive)) {
exclusionRegion = mDragResizeListener.getCornersRegion();
} else {
exclusionRegion = new Region();
}
+ if (inFullImmersive) {
+ // Task can't be moved in full immersive, so skip excluding the caption region.
+ return exclusionRegion;
+ }
exclusionRegion.union(new Rect(0, 0, mResult.mWidth,
getCaptionHeight(mTaskInfo.getWindowingMode())));
exclusionRegion.translate(mPositionInParent.x, mPositionInParent.y);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
new file mode 100644
index 0000000..cae6095
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.os.IBinder
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager.TRANSIT_CHANGE
+import android.window.WindowContainerTransaction
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/**
+ * Tests for [DesktopFullImmersiveTransitionHandler].
+ *
+ * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandler
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+
+ @Mock private lateinit var mockTransitions: Transitions
+ private lateinit var desktopRepository: DesktopRepository
+ private val transactionSupplier = { SurfaceControl.Transaction() }
+
+ private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
+
+ @Before
+ fun setUp() {
+ desktopRepository = DesktopRepository(
+ context, ShellInit(TestShellExecutor()), mock(), mock()
+ )
+ immersiveHandler = DesktopFullImmersiveTransitionHandler(
+ transitions = mockTransitions,
+ desktopRepository = desktopRepository,
+ transactionSupplier = transactionSupplier
+ )
+ }
+
+ @Test
+ fun enterImmersive_transitionReady_updatesRepository() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = false
+ )
+
+ immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.onTransitionReady(mockBinder)
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
+ }
+
+ @Test
+ fun exitImmersive_transitionReady_updatesRepository() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+ desktopRepository.setTaskInFullImmersiveState(
+ displayId = task.displayId,
+ taskId = task.taskId,
+ immersive = true
+ )
+
+ immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.onTransitionReady(mockBinder)
+
+ assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
+ }
+
+ @Test
+ fun enterImmersive_inProgress_ignores() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+
+ immersiveHandler.enterImmersive(task, wct)
+ immersiveHandler.enterImmersive(task, wct)
+
+ verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ }
+
+ @Test
+ fun exitImmersive_inProgress_ignores() {
+ val task = createFreeformTask()
+ val wct = WindowContainerTransaction()
+ val mockBinder = mock(IBinder::class.java)
+ whenever(mockTransitions.startTransition(TRANSIT_CHANGE, wct, immersiveHandler))
+ .thenReturn(mockBinder)
+
+ immersiveHandler.exitImmersive(task, wct)
+ immersiveHandler.exitImmersive(task, wct)
+
+ verify(mockTransitions, times(1)).startTransition(TRANSIT_CHANGE, wct, immersiveHandler)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 0ccd160..9e5c1a6 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -147,6 +147,7 @@
import org.mockito.Mockito.times
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
+import org.mockito.kotlin.argThat
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.capture
import org.mockito.kotlin.eq
@@ -183,6 +184,8 @@
@Mock
lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
@Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
+ @Mock
+ lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler
@Mock lateinit var launchAdjacentController: LaunchAdjacentController
@Mock lateinit var splitScreenController: SplitScreenController
@Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@@ -289,6 +292,7 @@
dragAndDropTransitionHandler,
toggleResizeDesktopTaskTransitionHandler,
dragToDesktopTransitionHandler,
+ mockDesktopFullImmersiveTransitionHandler,
taskRepository,
desktopModeLoggerTransitionObserver,
launchAdjacentController,
@@ -3123,6 +3127,30 @@
verify(shellController, times(1)).addUserChangeListener(any())
}
+ @Test
+ fun toggleImmersive_enter_resizesToDisplayBounds() {
+ val task = setUpFreeformTask(DEFAULT_DISPLAY)
+ taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, false /* immersive */)
+
+ controller.toggleDesktopTaskFullImmersiveState(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).enterImmersive(eq(task), argThat { wct ->
+ wct.hasBoundsChange(task.token, Rect())
+ })
+ }
+
+ @Test
+ fun toggleImmersive_exit_resizesToStableBounds() {
+ val task = setUpFreeformTask(DEFAULT_DISPLAY)
+ taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, true /* immersive */)
+
+ controller.toggleDesktopTaskFullImmersiveState(task)
+
+ verify(mockDesktopFullImmersiveTransitionHandler).exitImmersive(eq(task), argThat { wct ->
+ wct.hasBoundsChange(task.token, STABLE_BOUNDS)
+ })
+ }
+
/**
* Assert that an unhandled drag event launches a PendingIntent with the
* windowing mode and bounds we are expecting.
@@ -3488,6 +3516,13 @@
.isEqualTo(windowingMode)
}
+private fun WindowContainerTransaction.hasBoundsChange(
+ token: WindowContainerToken,
+ bounds: Rect
+): Boolean = this.changes.any { change ->
+ change.key == token.asBinder() && change.value.configuration.windowConfiguration.bounds == bounds
+}
+
private fun WindowContainerTransaction?.anyDensityConfigChange(
token: WindowContainerToken
): Boolean {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 499e339..77b2b0d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -17,6 +17,7 @@
package com.android.wm.shell.freeform;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
@@ -30,6 +31,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.IBinder;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.view.SurfaceControl;
import android.window.IWindowContainerToken;
import android.window.TransitionInfo;
@@ -37,28 +40,38 @@
import androidx.test.filters.SmallTest;
+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.TransitionInfoBuilder;
import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.windowdecor.WindowDecorViewModel;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.Optional;
+
/**
* Tests of {@link FreeformTaskTransitionObserver}
*/
@SmallTest
public class FreeformTaskTransitionObserverTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private ShellInit mShellInit;
@Mock
private Transitions mTransitions;
@Mock
+ private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler;
+ @Mock
private WindowDecorViewModel mWindowDecorViewModel;
private FreeformTaskTransitionObserver mTransitionObserver;
@@ -74,7 +87,9 @@
doReturn(pm).when(context).getPackageManager();
mTransitionObserver = new FreeformTaskTransitionObserver(
- context, mShellInit, mTransitions, mWindowDecorViewModel);
+ context, mShellInit, mTransitions,
+ Optional.of(mDesktopFullImmersiveTransitionHandler),
+ mWindowDecorViewModel);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
Runnable.class);
@@ -223,6 +238,19 @@
verify(mWindowDecorViewModel).destroyWindowDecoration(change2.getTaskInfo());
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ public void onTransitionReady_forwardsToDesktopImmersiveHandler() {
+ final IBinder transition = mock(IBinder.class);
+ final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build();
+ final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
+ final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class);
+
+ mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
+
+ verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition);
+ }
+
private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {
final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo();
taskInfo.taskId = taskId;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 83bd15b..4aa7e18 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -1221,9 +1221,48 @@
assertEquals(decor.mTaskInfo.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
}
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testMaximizeButtonClick_requestingImmersive_togglesDesktopImmersiveState() {
+ val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+ as ArgumentCaptor<View.OnClickListener>
+ val decor = createOpenTaskDecoration(
+ windowingMode = WINDOWING_MODE_FREEFORM,
+ onCaptionButtonClickListener = onClickListenerCaptor,
+ requestingImmersive = true,
+ )
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.maximize_window)
+
+ onClickListenerCaptor.value.onClick(view)
+
+ verify(mockDesktopTasksController)
+ .toggleDesktopTaskFullImmersiveState(decor.mTaskInfo)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+ fun testMaximizeButtonClick_notRequestingImmersive_togglesDesktopTaskSize() {
+ val onClickListenerCaptor = forClass(View.OnClickListener::class.java)
+ as ArgumentCaptor<View.OnClickListener>
+ val decor = createOpenTaskDecoration(
+ windowingMode = WINDOWING_MODE_FREEFORM,
+ onCaptionButtonClickListener = onClickListenerCaptor,
+ requestingImmersive = false,
+ )
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.maximize_window)
+
+ onClickListenerCaptor.value.onClick(view)
+
+ verify(mockDesktopTasksController)
+ .toggleDesktopTaskSize(decor.mTaskInfo)
+ }
+
private fun createOpenTaskDecoration(
@WindowingMode windowingMode: Int,
taskSurface: SurfaceControl = SurfaceControl(),
+ requestingImmersive: Boolean = false,
onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> =
forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>>,
onLeftSnapClickListenerCaptor: ArgumentCaptor<Function0<Unit>> =
@@ -1243,7 +1282,10 @@
onCaptionButtonTouchListener: ArgumentCaptor<View.OnTouchListener> =
forClass(View.OnTouchListener::class.java) as ArgumentCaptor<View.OnTouchListener>
): DesktopModeWindowDecoration {
- val decor = setUpMockDecorationForTask(createTask(windowingMode = windowingMode))
+ val decor = setUpMockDecorationForTask(createTask(
+ windowingMode = windowingMode,
+ requestingImmersive = requestingImmersive
+ ))
onTaskOpening(decor.mTaskInfo, taskSurface)
verify(decor).setOnMaximizeOrRestoreClickListener(onMaxOrRestoreListenerCaptor.capture())
verify(decor).setOnLeftSnapClickListener(onLeftSnapClickListenerCaptor.capture())
@@ -1282,6 +1324,7 @@
activityType: Int = ACTIVITY_TYPE_STANDARD,
focused: Boolean = true,
activityInfo: ActivityInfo = ActivityInfo(),
+ requestingImmersive: Boolean = false
): RunningTaskInfo {
return TestRunningTaskInfoBuilder()
.setDisplayId(displayId)
@@ -1292,6 +1335,11 @@
topActivityInfo = activityInfo
isFocused = focused
isResizeable = true
+ requestedVisibleTypes = if (requestingImmersive) {
+ statusBars().inv()
+ } else {
+ statusBars()
+ }
}
}