Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c715ed18 authored by Ivan Tkachenko's avatar Ivan Tkachenko
Browse files

Exit desktop windowing transition

- Added `FreeformTaskTransitionStarterInitializer` to extract transition
  starter setup logic from `FreeformTaskTransitionHandler`.
- Added `DesktopMixedTransitionHandler` to coordinate desktop task close
  transition animation between Launcher (via `dispatchTransition`),
  `CloseDesktopTaskTransitionHandler` and fallback `FreeformTaskTransitionHandler`.
- Added `CloseDesktopTaskTransitionHandler` to animate close desktop
  window transition, when not leaving desktop mode.

Bug: 331165070
Test: atest WMShellUnitTests:DesktopMixedTransitionHandlerTest WMShellUnitTests:CloseDesktopTaskTransitionHandlerTest
Flag: com.android.window.flags.enable_desktop_windowing_exit_transitions
Change-Id: Ic5bf3673a5f5e8ae2fa22a9d3dbe77cefd244cb9
parent 999765ce
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -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.
+1 −0
Original line number Diff line number Diff line
@@ -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",
+67 −17
Original line number Diff line number Diff line
@@ -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
    //
@@ -689,6 +703,16 @@ public abstract class WMShellModule {
                transitions, context, interactionJankMonitor, handler);
    }

    @WMSingleton
    @Provides
    static CloseDesktopTaskTransitionHandler provideCloseDesktopTaskTransitionHandler(
            Context context,
            @ShellMainThread ShellExecutor mainExecutor,
            @ShellAnimationThread ShellExecutor animExecutor
    ) {
        return new CloseDesktopTaskTransitionHandler(context, mainExecutor, animExecutor);
    }

    @WMSingleton
    @Provides
    static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler(
@@ -743,6 +767,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(
+153 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.desktopmode

import android.animation.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
    }
}
+157 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.desktopmode

import android.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"
    }
}
Loading