diff options
10 files changed, 628 insertions, 36 deletions
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 0dca97c8e73c..b7d967709123 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.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; @@ -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 @@ -621,6 +624,7 @@ public abstract class WMShellModule { ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, DragToDesktopTransitionHandler dragToDesktopTransitionHandler, @DynamicOverride DesktopRepository desktopRepository, + Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler, DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver, LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, @@ -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); @@ -671,6 +676,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( 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 000000000000..f749aa1edd92 --- /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 2303e71eced2..4e548a691907 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 @@ 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, @@ -231,6 +232,7 @@ class DesktopTasksController( toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener) dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener + immersiveTransitionHandler.onTaskResizeAnimationListener = listener } fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { @@ -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 @@ -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)) } @@ -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 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 ffcc526eac40..cb0354ecf13a 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 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; @@ -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, @@ -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 = @@ -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); @@ -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()) { 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 bf175b71ca05..bcf48d9ec2eb 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 @@ 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) { @@ -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. - 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 @@ 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: { - 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 @@ 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); @@ -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; } 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 c7e842244de2..25d37fce7270 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeManageWindowsMenu(); closeMaximizeMenu(); } - updateDragResizeListener(oldDecorationSurface); - updateMaximizeMenu(startT); + updateDragResizeListener(oldDecorationSurface, inFullImmersive); + updateMaximizeMenu(startT, inFullImmersive); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces } @@ -571,11 +571,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin 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 000000000000..cae609526c65 --- /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 0ccd160c7a91..9e5c1a66b823 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.verify 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 @@ class DesktopTasksControllerTest : ShellTestCase() { @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 @@ class DesktopTasksControllerTest : ShellTestCase() { dragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, + mockDesktopFullImmersiveTransitionHandler, taskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, @@ -3123,6 +3127,30 @@ class DesktopTasksControllerTest : ShellTestCase() { 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 @@ private fun WindowContainerTransaction.assertLaunchTaskAt( .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 499e339bc682..77b2b0d49006 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.app.ActivityManager; 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 android.window.WindowContainerToken; 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 @@ public class FreeformTaskTransitionObserverTest { 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 @@ public class FreeformTaskTransitionObserverTest { 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 83bd15b79186..4aa7e18b4b84 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { 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 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { activityType: Int = ACTIVITY_TYPE_STANDARD, focused: Boolean = true, activityInfo: ActivityInfo = ActivityInfo(), + requestingImmersive: Boolean = false ): RunningTaskInfo { return TestRunningTaskInfoBuilder() .setDisplayId(displayId) @@ -1292,6 +1335,11 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { topActivityInfo = activityInfo isFocused = focused isResizeable = true + requestedVisibleTypes = if (requestingImmersive) { + statusBars().inv() + } else { + statusBars() + } } } |