diff options
11 files changed, 814 insertions, 64 deletions
diff --git a/core/java/android/window/flags/DesktopModeFlags.java b/core/java/android/window/flags/DesktopModeFlags.java index 944a106bf441..47af50dac930 100644 --- a/core/java/android/window/flags/DesktopModeFlags.java +++ b/core/java/android/window/flags/DesktopModeFlags.java @@ -64,7 +64,8 @@ public enum DesktopModeFlags { ENABLE_WINDOWING_EDGE_DRAG_RESIZE(Flags::enableWindowingEdgeDragResize, true), ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS( Flags::enableDesktopWindowingTaskbarRunningApps, true), - ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false); + ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false), + ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false); private static final String TAG = "DesktopModeFlagsUtil"; // Function called to obtain aconfig flag value. diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index f8574294a3a2..a796eccd53ba 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -220,6 +220,7 @@ android_library { "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", "//frameworks/libs/systemui:iconloader_base", "com_android_wm_shell_flags_lib", + "PlatformAnimationLib", "WindowManager-Shell-proto", "WindowManager-Shell-lite-proto", "WindowManager-Shell-shared", 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 96a07755fea9..888fc62e7ce3 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 @@ -16,6 +16,7 @@ package com.android.wm.shell.dagger; +import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS; import static android.window.flags.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT; import android.annotation.Nullable; @@ -59,8 +60,10 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.dagger.back.ShellBackAnimationModule; 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.DesktopMixedTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; @@ -85,6 +88,8 @@ import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; import com.android.wm.shell.freeform.FreeformTaskTransitionObserver; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; +import com.android.wm.shell.freeform.FreeformTaskTransitionStarterInitializer; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.PipTransitionController; @@ -317,9 +322,13 @@ public abstract class WMShellModule { static FreeformComponents provideFreeformComponents( FreeformTaskListener taskListener, FreeformTaskTransitionHandler transitionHandler, - FreeformTaskTransitionObserver transitionObserver) { + FreeformTaskTransitionObserver transitionObserver, + FreeformTaskTransitionStarterInitializer transitionStarterInitializer) { return new FreeformComponents( - taskListener, Optional.of(transitionHandler), Optional.of(transitionObserver)); + taskListener, + Optional.of(transitionHandler), + Optional.of(transitionObserver), + Optional.of(transitionStarterInitializer)); } @WMSingleton @@ -343,27 +352,15 @@ public abstract class WMShellModule { @WMSingleton @Provides static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler( - ShellInit shellInit, Transitions transitions, - Context context, - WindowDecorViewModel windowDecorViewModel, DisplayController displayController, @ShellMainThread ShellExecutor mainExecutor, - @ShellAnimationThread ShellExecutor animExecutor, - @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, - InteractionJankMonitor interactionJankMonitor, - @ShellMainThread Handler handler) { + @ShellAnimationThread ShellExecutor animExecutor) { return new FreeformTaskTransitionHandler( - shellInit, transitions, - context, - windowDecorViewModel, displayController, mainExecutor, - animExecutor, - desktopModeTaskRepository, - interactionJankMonitor, - handler); + animExecutor); } @WMSingleton @@ -377,6 +374,23 @@ public abstract class WMShellModule { context, shellInit, transitions, windowDecorViewModel); } + @WMSingleton + @Provides + static FreeformTaskTransitionStarterInitializer provideFreeformTaskTransitionStarterInitializer( + ShellInit shellInit, + WindowDecorViewModel windowDecorViewModel, + FreeformTaskTransitionHandler freeformTaskTransitionHandler, + Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler) { + FreeformTaskTransitionStarter transitionStarter; + if (desktopMixedTransitionHandler.isPresent()) { + transitionStarter = desktopMixedTransitionHandler.get(); + } else { + transitionStarter = freeformTaskTransitionHandler; + } + return new FreeformTaskTransitionStarterInitializer(shellInit, windowDecorViewModel, + transitionStarter); + } + // // One handed mode // @@ -686,7 +700,17 @@ public abstract class WMShellModule { InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler) { return new ExitDesktopTaskTransitionHandler( - transitions, context, interactionJankMonitor, handler); + transitions, context, interactionJankMonitor, handler); + } + + @WMSingleton + @Provides + static CloseDesktopTaskTransitionHandler provideCloseDesktopTaskTransitionHandler( + Context context, + @ShellMainThread ShellExecutor mainExecutor, + @ShellAnimationThread ShellExecutor animExecutor + ) { + return new CloseDesktopTaskTransitionHandler(context, mainExecutor, animExecutor); } @WMSingleton @@ -745,6 +769,32 @@ public abstract class WMShellModule { @WMSingleton @Provides + static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler( + Context context, + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + FreeformTaskTransitionHandler freeformTaskTransitionHandler, + CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler, + InteractionJankMonitor interactionJankMonitor, + @ShellMainThread Handler handler + ) { + if (!DesktopModeStatus.canEnterDesktopMode(context) + || !ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()) { + return Optional.empty(); + } + return Optional.of( + new DesktopMixedTransitionHandler( + context, + transitions, + desktopModeTaskRepository, + freeformTaskTransitionHandler, + closeDesktopTaskTransitionHandler, + interactionJankMonitor, + handler)); + } + + @WMSingleton + @Provides static DesktopModeLoggerTransitionObserver provideDesktopModeLoggerTransitionObserver( Context context, ShellInit shellInit, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt new file mode 100644 index 000000000000..a16c15dfdf1a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandler.kt @@ -0,0 +1,153 @@ +/* + * 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.Animator +import android.animation.AnimatorSet +import android.animation.RectEvaluator +import android.animation.ValueAnimator +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context +import android.graphics.Rect +import android.os.IBinder +import android.util.TypedValue +import android.view.SurfaceControl.Transaction +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import androidx.core.animation.addListener +import com.android.app.animation.Interpolators +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.transition.Transitions +import java.util.function.Supplier + +/** The [Transitions.TransitionHandler] that handles transitions for closing desktop mode tasks. */ +class CloseDesktopTaskTransitionHandler +@JvmOverloads +constructor( + private val context: Context, + private val mainExecutor: ShellExecutor, + private val animExecutor: ShellExecutor, + private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }, +) : Transitions.TransitionHandler { + + private val runningAnimations = mutableMapOf<IBinder, List<Animator>>() + + /** Returns null, as it only handles transitions started from Shell. */ + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? = null + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: Transaction, + finishTransaction: Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + if (info.type != WindowManager.TRANSIT_CLOSE) return false + val animations = mutableListOf<Animator>() + val onAnimFinish: (Animator) -> Unit = { animator -> + mainExecutor.execute { + // Animation completed + animations.remove(animator) + if (animations.isEmpty()) { + // All animations completed, finish the transition + runningAnimations.remove(transition) + finishCallback.onTransitionFinished(/* wct= */ null) + } + } + } + animations += + info.changes + .filter { + it.mode == WindowManager.TRANSIT_CLOSE && + it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM + } + .map { createCloseAnimation(it, finishTransaction, onAnimFinish) } + if (animations.isEmpty()) return false + runningAnimations[transition] = animations + animExecutor.execute { animations.forEach(Animator::start) } + return true + } + + private fun createCloseAnimation( + change: TransitionInfo.Change, + finishTransaction: Transaction, + onAnimFinish: (Animator) -> Unit, + ): Animator { + finishTransaction.hide(change.leash) + return AnimatorSet().apply { + playTogether(createBoundsCloseAnimation(change), createAlphaCloseAnimation(change)) + addListener(onEnd = onAnimFinish) + } + } + + private fun createBoundsCloseAnimation(change: TransitionInfo.Change): Animator { + val startBounds = change.startAbsBounds + val endBounds = + Rect(startBounds).apply { + // Scale the end bounds of the window down with an anchor in the center + inset( + (startBounds.width().toFloat() * (1 - CLOSE_ANIM_SCALE) / 2).toInt(), + (startBounds.height().toFloat() * (1 - CLOSE_ANIM_SCALE) / 2).toInt() + ) + val offsetY = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + CLOSE_ANIM_OFFSET_Y, + context.resources.displayMetrics + ) + .toInt() + offset(/* dx= */ 0, offsetY) + } + return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply { + duration = CLOSE_ANIM_DURATION_BOUNDS + interpolator = Interpolators.STANDARD_ACCELERATE + addUpdateListener { animation -> + val animBounds = animation.animatedValue as Rect + val animScale = 1 - (1 - CLOSE_ANIM_SCALE) * animation.animatedFraction + transactionSupplier + .get() + .setPosition(change.leash, animBounds.left.toFloat(), animBounds.top.toFloat()) + .setScale(change.leash, animScale, animScale) + .apply() + } + } + } + + private fun createAlphaCloseAnimation(change: TransitionInfo.Change): Animator = + ValueAnimator.ofFloat(1f, 0f).apply { + duration = CLOSE_ANIM_DURATION_ALPHA + interpolator = Interpolators.LINEAR + addUpdateListener { animation -> + transactionSupplier + .get() + .setAlpha(change.leash, animation.animatedValue as Float) + .apply() + } + } + + private companion object { + const val CLOSE_ANIM_DURATION_BOUNDS = 200L + const val CLOSE_ANIM_DURATION_ALPHA = 100L + const val CLOSE_ANIM_SCALE = 0.95f + const val CLOSE_ANIM_OFFSET_Y = 36.0f + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt new file mode 100644 index 000000000000..ec3f8c5fc2f8 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -0,0 +1,157 @@ +/* + * 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.app.ActivityTaskManager.INVALID_TASK_ID +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.content.Context +import android.os.Handler +import android.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE +import com.android.internal.jank.InteractionJankMonitor +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.freeform.FreeformTaskTransitionHandler +import com.android.wm.shell.freeform.FreeformTaskTransitionStarter +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.MixedTransitionHandler +import com.android.wm.shell.transition.Transitions + +/** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */ +class DesktopMixedTransitionHandler( + private val context: Context, + private val transitions: Transitions, + private val desktopTaskRepository: DesktopModeTaskRepository, + private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler, + private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler, + private val interactionJankMonitor: InteractionJankMonitor, + @ShellMainThread private val handler: Handler, +) : MixedTransitionHandler, FreeformTaskTransitionStarter { + + /** Delegates starting transition to [FreeformTaskTransitionHandler]. */ + override fun startWindowingModeTransition( + targetWindowingMode: Int, + wct: WindowContainerTransaction?, + ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct) + + /** Delegates starting minimized mode transition to [FreeformTaskTransitionHandler]. */ + override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder = + freeformTaskTransitionHandler.startMinimizedModeTransition(wct) + + /** Starts close transition and handles or delegates desktop task close animation. */ + override fun startRemoveTransition(wct: WindowContainerTransaction?) { + requireNotNull(wct) + transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this) + } + + /** Returns null, as it only handles transitions started from Shell. */ + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? = null + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + val closeChange = findCloseDesktopTaskChange(info) + if (closeChange == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: Should have closing desktop task", TAG) + return false + } + if (isLastDesktopTask(closeChange)) { + // Dispatch close desktop task animation to the default transition handlers. + return dispatchCloseLastDesktopTaskAnimation( + transition, + info, + closeChange, + startTransaction, + finishTransaction, + finishCallback, + ) + } + // Animate close desktop task transition with [CloseDesktopTaskTransitionHandler]. + return closeDesktopTaskTransitionHandler.startAnimation( + transition, + info, + startTransaction, + finishTransaction, + finishCallback, + ) + } + + /** + * Dispatch close desktop task animation to the default transition handlers. Allows delegating + * it to Launcher to animate in sync with show Home transition. + */ + private fun dispatchCloseLastDesktopTaskAnimation( + transition: IBinder, + info: TransitionInfo, + change: TransitionInfo.Change, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + // Starting the jank trace if closing the last window in desktop mode. + interactionJankMonitor.begin( + change.leash, + context, + handler, + CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE, + ) + // Dispatch the last desktop task closing animation. + return transitions.dispatchTransition( + transition, + info, + startTransaction, + finishTransaction, + { wct -> + // Finish the jank trace when closing the last window in desktop mode. + interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE) + finishCallback.onTransitionFinished(wct) + }, + /* skip= */ this + ) != null + } + + private fun isLastDesktopTask(change: TransitionInfo.Change): Boolean = + change.taskInfo?.let { + desktopTaskRepository.getActiveNonMinimizedTaskCount(it.displayId) == 1 + } ?: false + + private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? { + if (info.type != WindowManager.TRANSIT_CLOSE) return null + return info.changes.firstOrNull { change -> + change.mode == WindowManager.TRANSIT_CLOSE && + !change.hasFlags(TransitionInfo.FLAG_IS_WALLPAPER) && + change.taskInfo?.taskId != INVALID_TASK_ID && + change.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM + } + } + + companion object { + private const val TAG = "DesktopMixedTransitionHandler" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java index eee5aaee3ec3..3379ff2a8c30 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformComponents.java @@ -35,6 +35,7 @@ public class FreeformComponents { public final ShellTaskOrganizer.TaskListener mTaskListener; public final Optional<Transitions.TransitionHandler> mTransitionHandler; public final Optional<Transitions.TransitionObserver> mTransitionObserver; + public final Optional<FreeformTaskTransitionStarterInitializer> mTransitionStarterInitializer; /** * Creates an instance with the given components. @@ -42,10 +43,12 @@ public class FreeformComponents { public FreeformComponents( ShellTaskOrganizer.TaskListener taskListener, Optional<Transitions.TransitionHandler> transitionHandler, - Optional<Transitions.TransitionObserver> transitionObserver) { + Optional<Transitions.TransitionObserver> transitionObserver, + Optional<FreeformTaskTransitionStarterInitializer> transitionStarterInitializer) { mTaskListener = taskListener; mTransitionHandler = transitionHandler; mTransitionObserver = transitionObserver; + mTransitionStarterInitializer = transitionStarterInitializer; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index 517e20910f6d..6aaf001d46f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -19,16 +19,12 @@ package com.android.wm.shell.freeform; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.ActivityManager; import android.app.WindowConfiguration; -import android.content.Context; import android.graphics.Rect; -import android.os.Handler; import android.os.IBinder; import android.util.ArrayMap; import android.view.SurfaceControl; @@ -40,14 +36,9 @@ import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; -import com.android.wm.shell.shared.annotations.ShellMainThread; -import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; -import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.util.ArrayList; import java.util.List; @@ -59,48 +50,24 @@ import java.util.List; public class FreeformTaskTransitionHandler implements Transitions.TransitionHandler, FreeformTaskTransitionStarter { private static final int CLOSE_ANIM_DURATION = 400; - private final Context mContext; private final Transitions mTransitions; - private final WindowDecorViewModel mWindowDecorViewModel; - private final DesktopModeTaskRepository mDesktopModeTaskRepository; private final DisplayController mDisplayController; - private final InteractionJankMonitor mInteractionJankMonitor; private final ShellExecutor mMainExecutor; private final ShellExecutor mAnimExecutor; - @ShellMainThread - private final Handler mHandler; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); private final ArrayMap<IBinder, ArrayList<Animator>> mAnimations = new ArrayMap<>(); public FreeformTaskTransitionHandler( - ShellInit shellInit, Transitions transitions, - Context context, - WindowDecorViewModel windowDecorViewModel, DisplayController displayController, ShellExecutor mainExecutor, - ShellExecutor animExecutor, - DesktopModeTaskRepository desktopModeTaskRepository, - InteractionJankMonitor interactionJankMonitor, - @ShellMainThread Handler handler) { + ShellExecutor animExecutor) { mTransitions = transitions; - mContext = context; - mWindowDecorViewModel = windowDecorViewModel; - mDesktopModeTaskRepository = desktopModeTaskRepository; mDisplayController = displayController; - mInteractionJankMonitor = interactionJankMonitor; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; - mHandler = handler; - if (Transitions.ENABLE_SHELL_TRANSITIONS) { - shellInit.addInitCallback(this::onInit, this); - } - } - - private void onInit() { - mWindowDecorViewModel.setFreeformTaskTransitionStarter(this); } @Override @@ -269,20 +236,12 @@ public class FreeformTaskTransitionHandler startBounds.top + (animation.getAnimatedFraction() * screenHeight)); t.apply(); }); - if (mDesktopModeTaskRepository.getActiveNonMinimizedTaskCount( - change.getTaskInfo().displayId) == 1) { - // Starting the jank trace if closing the last window in desktop mode. - mInteractionJankMonitor.begin( - sc, mContext, mHandler, CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE); - } animator.addListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { animations.remove(animator); onAnimFinish.run(); - mInteractionJankMonitor.end( - CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE); } }); animations.add(animator); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt new file mode 100644 index 000000000000..98bdf059e738 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt @@ -0,0 +1,41 @@ +/* + * 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.freeform + +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.windowdecor.WindowDecorViewModel + +/** + * Sets up [FreeformTaskTransitionStarter] for [WindowDecorViewModel] when shell finishes + * initializing. + * + * Used to extract the setup logic from the starter implementation. + */ +class FreeformTaskTransitionStarterInitializer( + shellInit: ShellInit, + private val windowDecorViewModel: WindowDecorViewModel, + private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter +) { + init { + shellInit.addInitCallback(::onShellInit, this) + } + + /** Sets up [WindowDecorViewModel] transition starter with [FreeformTaskTransitionStarter] */ + private fun onShellInit() { + windowDecorViewModel.setFreeformTaskTransitionStarter(freeformTaskTransitionStarter) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index d03832d3e85e..70aaac4e8f77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -1026,9 +1026,14 @@ public class Transitions implements RemoteCallable<Transitions>, * Gives every handler (in order) a chance to animate until one consumes the transition. * @return the handler which consumed the transition. */ - TransitionHandler dispatchTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, - @NonNull TransitionFinishCallback finishCB, @Nullable TransitionHandler skip) { + public TransitionHandler dispatchTransition( + @NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull TransitionFinishCallback finishCB, + @Nullable TransitionHandler skip + ) { for (int i = mHandlers.size() - 1; i >= 0; --i) { if (mHandlers.get(i) == skip) continue; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s", diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt new file mode 100644 index 000000000000..9b4cc17e19d9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt @@ -0,0 +1,160 @@ +/* + * 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.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WindowingMode +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.common.ShellExecutor +import java.util.function.Supplier +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.kotlin.mock + +/** + * Test class for [CloseDesktopTaskTransitionHandler] + * + * Usage: atest WMShellUnitTests:CloseDesktopTaskTransitionHandlerTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { + + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var closingTaskLeash: SurfaceControl + + private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } + + private lateinit var handler: CloseDesktopTaskTransitionHandler + + @Before + fun setUp() { + handler = + CloseDesktopTaskTransitionHandler( + context, + testExecutor, + testExecutor, + transactionSupplier + ) + } + + @Test + fun handleRequest_returnsNull() { + assertNull(handler.handleRequest(mock(), mock())) + } + + @Test + fun startAnimation_openTransition_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + createTransitionInfo( + type = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate open transition", animates) + } + + @Test + fun startAnimation_closeTransitionFullscreenTask_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate fullscreen task close transition", animates) + } + + @Test + fun startAnimation_closeTransitionOpeningFreeformTask_returnsFalse() { + val animates = + handler.startAnimation( + transition = mock(), + info = + createTransitionInfo( + changeMode = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not animate opening freeform task close transition", animates) + } + + @Test + fun startAnimation_closeTransitionClosingFreeformTask_returnsTrue() { + val animates = + handler.startAnimation( + transition = mock(), + info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should animate closing freeform task close transition", animates) + } + + private fun createTransitionInfo( + type: Int = WindowManager.TRANSIT_CLOSE, + changeMode: Int = WindowManager.TRANSIT_CLOSE, + task: RunningTaskInfo + ): TransitionInfo = + TransitionInfo(type, 0 /* flags */).apply { + addChange( + TransitionInfo.Change(mock(), closingTaskLeash).apply { + mode = changeMode + parent = null + taskInfo = task + } + ) + } + + private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(windowingMode) + .build() +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt new file mode 100644 index 000000000000..2b60200f06ad --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -0,0 +1,220 @@ +/* + * 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.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WindowingMode +import android.os.Handler +import android.os.IBinder +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE +import com.android.internal.jank.InteractionJankMonitor +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.freeform.FreeformTaskTransitionHandler +import com.android.wm.shell.transition.Transitions +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Test class for [DesktopMixedTransitionHandler] + * + * Usage: atest WMShellUnitTests:DesktopMixedTransitionHandlerTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopMixedTransitionHandlerTest : ShellTestCase() { + + @Mock lateinit var transitions: Transitions + @Mock lateinit var desktopTaskRepository: DesktopModeTaskRepository + @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler + @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler + @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + @Mock lateinit var mockHandler: Handler + @Mock lateinit var closingTaskLeash: SurfaceControl + + private lateinit var mixedHandler: DesktopMixedTransitionHandler + + @Before + fun setUp() { + mixedHandler = + DesktopMixedTransitionHandler( + context, + transitions, + desktopTaskRepository, + freeformTaskTransitionHandler, + closeDesktopTaskTransitionHandler, + interactionJankMonitor, + mockHandler + ) + } + + @Test + fun startWindowingModeTransition_callsFreeformTaskTransitionHandler() { + val windowingMode = WINDOWING_MODE_FULLSCREEN + val wct = WindowContainerTransaction() + + mixedHandler.startWindowingModeTransition(windowingMode, wct) + + verify(freeformTaskTransitionHandler).startWindowingModeTransition(windowingMode, wct) + } + + @Test + fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() { + val wct = WindowContainerTransaction() + whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any())) + .thenReturn(mock()) + + mixedHandler.startMinimizedModeTransition(wct) + + verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct) + } + + @Test + fun startRemoveTransition_startsCloseTransition() { + val wct = WindowContainerTransaction() + + mixedHandler.startRemoveTransition(wct) + + verify(transitions).startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler) + } + + @Test + fun handleRequest_returnsNull() { + assertNull(mixedHandler.handleRequest(mock(), mock())) + } + + @Test + fun startAnimation_withoutClosingDesktopTask_returnsFalse() { + val transition = mock<IBinder>() + val transitionInfo = + createTransitionInfo( + changeMode = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM) + ) + whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())) + .thenReturn(true) + + val started = mixedHandler.startAnimation( + transition = transition, + info = transitionInfo, + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertFalse("Should not start animation without closing desktop task", started) + } + + @Test + fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() { + val transition = mock<IBinder>() + val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)) + whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(2) + whenever( + closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()) + ) + .thenReturn(true) + + val started = mixedHandler.startAnimation( + transition = transition, + info = transitionInfo, + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + assertTrue("Should delegate animation to close transition handler", started) + verify(closeDesktopTaskTransitionHandler) + .startAnimation(eq(transition), eq(transitionInfo), any(), any(), any()) + } + + @Test + fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() { + val transition = mock<IBinder>() + val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)) + whenever(desktopTaskRepository.getActiveNonMinimizedTaskCount(any())).thenReturn(1) + whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any())) + .thenReturn(mock()) + + mixedHandler.startAnimation( + transition = transition, + info = transitionInfo, + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {} + ) + + verify(transitions) + .dispatchTransition( + eq(transition), + eq(transitionInfo), + any(), + any(), + any(), + eq(mixedHandler) + ) + verify(interactionJankMonitor) + .begin( + closingTaskLeash, + context, + mockHandler, + CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE + ) + } + + private fun createTransitionInfo( + type: Int = WindowManager.TRANSIT_CLOSE, + changeMode: Int = WindowManager.TRANSIT_CLOSE, + task: RunningTaskInfo + ): TransitionInfo = + TransitionInfo(type, 0 /* flags */).apply { + addChange( + TransitionInfo.Change(mock(), closingTaskLeash).apply { + mode = changeMode + parent = null + taskInfo = task + } + ) + } + + private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(windowingMode) + .build() +} |