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

Commit 61433dd0 authored by Jorge Gil's avatar Jorge Gil
Browse files

Add DesktopFullImmersiveTransitionHandler

To enter in/out of desktop's full immersive mode.

Also hooks up the transition handler to observe onTransitionReady from
FreeformTaskTransitionObserver, so that the repository state that tracks
immersive mode can be updated and thus window decorations can read the
immersive state to decide which UI to render on relayout.

Flag: com.android.window.flags.enable_fully_immersive_in_desktop
Bug: 369403798
Bug: 369443668
Test: atest WMShellUnitTests
Change-Id: I4aa1ef162a62359c3d22b0e2b0f1a40d716921ec
parent 10971ff2
Loading
Loading
Loading
Loading
+20 −2
Original line number Diff line number Diff line
@@ -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);
@@ -669,6 +674,19 @@ public abstract class WMShellModule {
        );
    }

    @WMSingleton
    @Provides
    static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
            Context context,
            Transitions transitions,
            @DynamicOverride DesktopRepository desktopRepository) {
        if (DesktopModeStatus.canEnterDesktopMode(context)) {
            return Optional.of(
                    new DesktopFullImmersiveTransitionHandler(transitions, desktopRepository));
        }
        return Optional.empty();
    }

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

import android.animation.RectEvaluator
import android.animation.ValueAnimator
import android.app.ActivityManager.RunningTaskInfo
import android.graphics.Rect
import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CHANGE
import android.view.animation.DecelerateInterpolator
import android.window.TransitionInfo
import android.window.TransitionRequestInfo
import android.window.WindowContainerTransaction
import androidx.core.animation.addListener
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.protolog.ShellProtoLogGroup
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TransitionHandler
import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener

/**
 * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task
 * remains freeform while being able to take fullscreen bounds and have its App Header visibility
 * be transient below the status bar like in fullscreen immersive mode.
 */
class DesktopFullImmersiveTransitionHandler(
    private val transitions: Transitions,
    private val desktopRepository: DesktopRepository,
    private val transactionSupplier: () -> SurfaceControl.Transaction,
) : TransitionHandler {

    constructor(
        transitions: Transitions,
        desktopRepository: DesktopRepository,
    ) : this(transitions, desktopRepository, { SurfaceControl.Transaction() })

    private var state: TransitionState? = null

    /** Whether there is an immersive transition that hasn't completed yet. */
    private val inProgress: Boolean
        get() = state != null

    private val rectEvaluator = RectEvaluator()

    /** A listener to invoke on animation changes during entry/exit. */
    var onTaskResizeAnimationListener: OnTaskResizeAnimationListener? = null

    /** Starts a transition to enter full immersive state inside the desktop. */
    fun enterImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
        if (inProgress) {
            ProtoLog.v(
                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                "FullImmersive: cannot start entry because transition already in progress."
            )
            return
        }

        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
        state = TransitionState(
            transition = transition,
            displayId = taskInfo.displayId,
            taskId = taskInfo.taskId,
            direction = Direction.ENTER
        )
    }

    fun exitImmersive(taskInfo: RunningTaskInfo, wct: WindowContainerTransaction) {
        if (inProgress) {
            ProtoLog.v(
                ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
                "$TAG: cannot start exit because transition already in progress."
            )
            return
        }

        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ this)
        state = TransitionState(
            transition = transition,
            displayId = taskInfo.displayId,
            taskId = taskInfo.taskId,
            direction = Direction.EXIT
        )
    }

    override fun startAnimation(
        transition: IBinder,
        info: TransitionInfo,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction,
        finishCallback: Transitions.TransitionFinishCallback
    ): Boolean {
        val state = requireState()
        if (transition != state.transition) return false
        animateResize(
            transitionState = state,
            info = info,
            startTransaction = startTransaction,
            finishTransaction = finishTransaction,
            finishCallback = finishCallback
        )
        return true
    }

    private fun animateResize(
        transitionState: TransitionState,
        info: TransitionInfo,
        startTransaction: SurfaceControl.Transaction,
        finishTransaction: SurfaceControl.Transaction,
        finishCallback: Transitions.TransitionFinishCallback
    ) {
        val change = info.changes.first { c ->
            val taskInfo = c.taskInfo
            return@first taskInfo != null && taskInfo.taskId == transitionState.taskId
        }
        val leash = change.leash
        val startBounds = change.startAbsBounds
        val endBounds = change.endAbsBounds

        val updateTransaction = transactionSupplier()
        ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply {
            duration = FULL_IMMERSIVE_ANIM_DURATION_MS
            interpolator = DecelerateInterpolator()
            addListener(
                onStart = {
                    startTransaction
                        .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
                        .setWindowCrop(leash, startBounds.width(), startBounds.height())
                        .show(leash)
                    onTaskResizeAnimationListener
                        ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds)
                        ?: startTransaction.apply()
                },
                onEnd = {
                    finishTransaction
                        .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat())
                        .setWindowCrop(leash, endBounds.width(), endBounds.height())
                        .apply()
                    onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId)
                    finishCallback.onTransitionFinished(null /* wct */)
                    clearState()
                }
            )
            addUpdateListener { animation ->
                val rect = animation.animatedValue as Rect
                updateTransaction
                    .setPosition(leash, rect.left.toFloat(), rect.top.toFloat())
                    .setWindowCrop(leash, rect.width(), rect.height())
                    .apply()
                onTaskResizeAnimationListener
                    ?.onBoundsChange(transitionState.taskId, updateTransaction, rect)
                    ?: updateTransaction.apply()
            }
            start()
        }
    }

    override fun handleRequest(
        transition: IBinder,
        request: TransitionRequestInfo
    ): WindowContainerTransaction? = null

    override fun onTransitionConsumed(
        transition: IBinder,
        aborted: Boolean,
        finishTransaction: SurfaceControl.Transaction?
    ) {
        val state = this.state ?: return
        if (transition == state.transition && aborted) {
            clearState()
        }
        super.onTransitionConsumed(transition, aborted, finishTransaction)
    }

    /**
     * Called when any transition in the system is ready to play. This is needed to update the
     * repository state before window decorations are drawn (which happens immediately after
     * |onTransitionReady|, before this transition actually animates) because drawing decorations
     * depends in whether the task is in full immersive state or not.
     */
    fun onTransitionReady(transition: IBinder) {
        val state = this.state ?: return
        // TODO: b/369443668 - this assumes invoking the exit transition is the only way to exit
        //  immersive, which isn't realistic. The app could crash, the user could dismiss it from
        //  overview, etc. This (or its caller) should search all transitions to look for any
        //  immersive task exiting that state to keep the repository properly updated.
        if (transition == state.transition) {
            when (state.direction) {
                Direction.ENTER -> {
                    desktopRepository.setTaskInFullImmersiveState(
                        displayId = state.displayId,
                        taskId = state.taskId,
                        immersive = true
                    )
                }
                Direction.EXIT -> {
                    desktopRepository.setTaskInFullImmersiveState(
                        displayId = state.displayId,
                        taskId = state.taskId,
                        immersive = false
                    )
                }
            }
        }
    }

    private fun clearState() {
        state = null
    }

    private fun requireState(): TransitionState =
        state ?: error("Expected non-null transition state")

    /** The state of the currently running transition. */
    private data class TransitionState(
        val transition: IBinder,
        val displayId: Int,
        val taskId: Int,
        val direction: Direction
    )

    private enum class Direction {
        ENTER, EXIT
    }

    private companion object {
        private const val TAG = "FullImmersiveHandler"

        private const val FULL_IMMERSIVE_ANIM_DURATION_MS = 336L
    }
}
+46 −12
Original line number Diff line number Diff line
@@ -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
+13 −0
Original line number Diff line number Diff line
@@ -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()) {
+18 −1
Original line number Diff line number Diff line
@@ -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.
                if (Flags.enableFullyImmersiveInDesktop()
                        && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) {
                    // Task is requesting immersive, so it should either enter or exit immersive,
                    // depending on immersive state.
                    onEnterOrExitImmersive(decoration.mTaskInfo.taskId);
                } else {
                    // Full immersive is disabled or task doesn't request/support it, so just
                    // toggle between maximize/restore states.
                    onMaximizeOrRestore(decoration.mTaskInfo.taskId, "caption_bar_button");
                }
            } else if (id == R.id.minimize_window) {
                final WindowContainerTransaction wct = new WindowContainerTransaction();
                mDesktopTasksController.onDesktopWindowMinimize(wct, mTaskId);
Loading