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

Commit 90bb564a authored by Jorge Gil's avatar Jorge Gil Committed by Android (Google) Code Review
Browse files

Merge changes from topic "desktoo-immersive-handler" into main

* changes:
  Add DesktopFullImmersiveTransitionHandler
  Disable App Header drag-move and drag-resize in full immersive
parents 5718f500 61433dd0
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()) {
+35 −7
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);
@@ -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: {
                    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;
        }
Loading