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

Commit fed05a65 authored by Merissa Mitchell's avatar Merissa Mitchell
Browse files

[PiP on Desktop] Handle Desktop cleanups for all PiP transitions.

Previously, Desktop cleanup was only initiated when necessary (if task
going to PiP is the last task) when PiP is entered via the minimize
button on the app handle.

This CL updates the implementation so that PiP transitions detected
by PipTransition#handleRequest would also be checked, i.e. when PiP is
entered via back navigation.

For all cases, if Desktop session is active _and_ the task going to PiP
is the last task, the WCT for the PiP transition is modified to include
Desktop exit cleanups.

Bug: 359906472
Bug: 399976327
Test: atest DesktopPipTransitionControllerTest
DesktopTasksControllerTest
Flag: com.android.window.flags.enable_desktop_windowing_pip

Change-Id: I36fbc34a04ea418ce10683ad2cdae6681df1fcc9
parent a14b7f9f
Loading
Loading
Loading
Loading
+0 −18
Original line number Diff line number Diff line
@@ -100,7 +100,6 @@ import com.android.wm.shell.desktopmode.DesktopModeKeyGestureHandler;
import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopModeMoveToDisplayTransitionHandler;
import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger;
import com.android.wm.shell.desktopmode.DesktopPipTransitionObserver;
import com.android.wm.shell.desktopmode.DesktopTaskChangeListener;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopTasksLimiter;
@@ -783,7 +782,6 @@ public abstract class WMShellModule {
            OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
            DesksOrganizer desksOrganizer,
            Optional<DesksTransitionObserver> desksTransitionObserver,
            Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver,
            UserProfileContexts userProfileContexts,
            DesktopModeCompatPolicy desktopModeCompatPolicy,
            DragToDisplayTransitionHandler dragToDisplayTransitionHandler,
@@ -827,7 +825,6 @@ public abstract class WMShellModule {
                overviewToDesktopTransitionObserver,
                desksOrganizer,
                desksTransitionObserver.get(),
                desktopPipTransitionObserver,
                userProfileContexts,
                desktopModeCompatPolicy,
                dragToDisplayTransitionHandler,
@@ -1240,7 +1237,6 @@ public abstract class WMShellModule {
            Transitions transitions,
            ShellTaskOrganizer shellTaskOrganizer,
            Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
            Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver,
            Optional<BackAnimationController> backAnimationController,
            DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
            ShellInit shellInit) {
@@ -1253,7 +1249,6 @@ public abstract class WMShellModule {
                                        transitions,
                                        shellTaskOrganizer,
                                        desktopMixedTransitionHandler.get(),
                                        desktopPipTransitionObserver,
                                        backAnimationController.get(),
                                        desktopWallpaperActivityTokenProvider,
                                        shellInit)));
@@ -1273,19 +1268,6 @@ public abstract class WMShellModule {
        return Optional.empty();
    }

    @WMSingleton
    @Provides
    static Optional<DesktopPipTransitionObserver> provideDesktopPipTransitionObserver(
            Context context
    ) {
        if (DesktopModeStatus.canEnterDesktopMode(context)
                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()) {
            return Optional.of(
                    new DesktopPipTransitionObserver());
        }
        return Optional.empty();
    }

    @WMSingleton
    @Provides
    static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler(
+22 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.wm.shell.dagger.pip;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Handler;
import android.window.DesktopModeFlags;

import com.android.internal.jank.InteractionJankMonitor;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
@@ -42,6 +43,8 @@ import com.android.wm.shell.common.pip.PipUtils;
import com.android.wm.shell.common.pip.SizeSpecSource;
import com.android.wm.shell.dagger.WMShellBaseModule;
import com.android.wm.shell.dagger.WMSingleton;
import com.android.wm.shell.desktopmode.DesktopPipTransitionController;
import com.android.wm.shell.desktopmode.DesktopTasksController;
import com.android.wm.shell.desktopmode.DesktopUserRepositories;
import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler;
import com.android.wm.shell.pip2.phone.PhonePipMenuController;
@@ -55,6 +58,7 @@ import com.android.wm.shell.pip2.phone.PipTransition;
import com.android.wm.shell.pip2.phone.PipTransitionState;
import com.android.wm.shell.pip2.phone.PipUiStateChangeController;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
import com.android.wm.shell.splitscreen.SplitScreenController;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
@@ -91,12 +95,13 @@ public abstract class Pip2Module {
            DisplayController displayController,
            Optional<SplitScreenController> splitScreenControllerOptional,
            PipDesktopState pipDesktopState,
            Optional<DesktopPipTransitionController> desktopPipTransitionController,
            PipInteractionHandler pipInteractionHandler) {
        return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
                pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener,
                pipScheduler, pipStackListenerController, pipDisplayLayoutState,
                pipUiStateChangeController, displayController, splitScreenControllerOptional,
                pipDesktopState, pipInteractionHandler);
                pipDesktopState, desktopPipTransitionController, pipInteractionHandler);
    }

    @WMSingleton
@@ -250,6 +255,22 @@ public abstract class Pip2Module {
                dragToDesktopTransitionHandlerOptional, rootTaskDisplayAreaOrganizer);
    }

    @WMSingleton
    @Provides
    static Optional<DesktopPipTransitionController> provideDesktopPipTransitionController(
            Context context, Optional<DesktopTasksController> desktopTasksControllerOptional,
            Optional<DesktopUserRepositories> desktopUserRepositoriesOptional,
            PipDesktopState pipDesktopState
    ) {
        if (DesktopModeStatus.canEnterDesktopMode(context)
                && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue()) {
            return Optional.of(
                    new DesktopPipTransitionController(desktopTasksControllerOptional.get(),
                            desktopUserRepositoriesOptional.get(), pipDesktopState));
        }
        return Optional.empty();
    }

    @BindsOptionalOf
    abstract DragToDesktopTransitionHandler optionalDragToDesktopTransitionHandler();

+115 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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
import android.os.IBinder
import android.window.DesktopExperienceFlags
import android.window.WindowContainerTransaction
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.common.pip.PipDesktopState
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE

/**
 * Controller to perform extra handling to PiP transitions that are entering while in Desktop mode.
 */
class DesktopPipTransitionController(
    private val desktopTasksController: DesktopTasksController,
    private val desktopUserRepositories: DesktopUserRepositories,
    private val pipDesktopState: PipDesktopState,
) {

    /**
     * This is called by [PipTransition#handleRequest] when a request for entering PiP is received.
     *
     * @param wct WindowContainerTransaction that will apply these changes
     * @param transition that will apply this transaction
     * @param taskInfo of the task that is entering PiP
     */
    fun handlePipTransition(
        wct: WindowContainerTransaction,
        transition: IBinder,
        taskInfo: ActivityManager.RunningTaskInfo,
    ) {
        if (!pipDesktopState.isDesktopWindowingPipEnabled()) {
            return
        }

        // Early return if the transition is a synthetic transition that is not backed by a true
        // system transition.
        if (transition == DesktopTasksController.SYNTHETIC_TRANSITION) {
            logD("handlePipTransitionIfInDesktop: SYNTHETIC_TRANSITION, not a true transition")
            return
        }

        val taskId = taskInfo.taskId
        val displayId = taskInfo.displayId
        val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId)
        if (!desktopRepository.isAnyDeskActive(displayId)) {
            logD("handlePipTransitionIfInDesktop: PiP transition is not in Desktop session")
            return
        }

        val deskId =
            desktopRepository.getActiveDeskId(displayId)
                ?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                    logW(
                        "handlePipTransitionIfInDesktop: " +
                            "Active desk not found for display id %d",
                        displayId,
                    )
                    return
                } else {
                    checkNotNull(desktopRepository.getDefaultDeskId(displayId)) {
                        "$TAG: handlePipTransitionIfInDesktop: " +
                            "Expected a default desk to exist in display with id $displayId"
                    }
                }

        val isLastTask =
            desktopRepository.isOnlyVisibleNonClosingTaskInDesk(
                taskId = taskId,
                deskId = deskId,
                displayId = displayId,
            )
        if (!isLastTask) {
            logD("handlePipTransitionIfInDesktop: PiP task is not last visible task in Desk")
            return
        }

        val desktopExitRunnable =
            desktopTasksController.performDesktopExitCleanUp(
                wct = wct,
                deskId = deskId,
                displayId = displayId,
                willExitDesktop = true,
            )
        desktopExitRunnable?.invoke(transition)
    }

    private fun logW(msg: String, vararg arguments: Any?) {
        ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    private fun logD(msg: String, vararg arguments: Any?) {
        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    private companion object {
        private const val TAG = "DesktopPipTransitionController"
    }
}
+0 −81
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.WindowConfiguration.WINDOWING_MODE_PINNED
import android.os.IBinder
import android.window.DesktopModeFlags
import android.window.TransitionInfo
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE

/**
 * Observer of PiP in Desktop Mode transitions. At the moment, this is specifically tracking a PiP
 * transition for a task that is entering PiP via the minimize button on the caption bar.
 */
class DesktopPipTransitionObserver {
    private val pendingPipTransitions = mutableMapOf<IBinder, PendingPipTransition>()

    /** Adds a pending PiP transition to be tracked. */
    fun addPendingPipTransition(transition: PendingPipTransition) {
        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return
        pendingPipTransitions[transition.token] = transition
    }

    /**
     * Called when any transition is ready, which may include transitions not tracked by this
     * observer.
     */
    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
        if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue) return
        val pipTransition = pendingPipTransitions.remove(transition) ?: return

        logD("Desktop PiP transition ready: %s", transition)
        for (change in info.changes) {
            val taskInfo = change.taskInfo
            if (taskInfo == null || taskInfo.taskId == -1) {
                continue
            }

            if (
                taskInfo.taskId == pipTransition.taskId &&
                    taskInfo.windowingMode == WINDOWING_MODE_PINNED
            ) {
                logD("Desktop PiP transition was successful")
                pipTransition.onSuccess()
                return
            }
        }
        logD("Change with PiP task not found in Desktop PiP transition; likely failed")
    }

    /**
     * Data tracked for a pending PiP transition.
     *
     * @property token the PiP transition that is started.
     * @property taskId task id of the task entering PiP.
     * @property onSuccess callback to be invoked if the PiP transition is successful.
     */
    data class PendingPipTransition(val token: IBinder, val taskId: Int, val onSuccess: () -> Unit)

    private fun logD(msg: String, vararg arguments: Any?) {
        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    private companion object {
        private const val TAG = "DesktopPipTransitionObserver"
    }
}
+22 −51
Original line number Diff line number Diff line
@@ -214,7 +214,6 @@ class DesktopTasksController(
    private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
    private val desksOrganizer: DesksOrganizer,
    private val desksTransitionObserver: DesksTransitionObserver,
    private val desktopPipTransitionObserver: Optional<DesktopPipTransitionObserver>,
    private val userProfileContexts: UserProfileContexts,
    private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
    private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
@@ -842,7 +841,6 @@ class DesktopTasksController(
            }
        val isMinimizingToPip =
            DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
                desktopPipTransitionObserver.isPresent &&
                (taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false)

        // If task is going to PiP, start a PiP transition instead of a minimize transition
@@ -856,25 +854,23 @@ class DesktopTasksController(
                    /* displayChange= */ null,
                    /* flags= */ 0,
                )
            val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
            val requestRes =
                transitions.dispatchRequest(SYNTHETIC_TRANSITION, requestInfo, /* skip= */ null)
            wct.merge(requestRes.second, true)

            desktopPipTransitionObserver
                .get()
                .addPendingPipTransition(
                    DesktopPipTransitionObserver.PendingPipTransition(
                        token = freeformTaskTransitionStarter.startPipTransition(wct),
                        taskId = taskInfo.taskId,
                        onSuccess = {
                            onDesktopTaskEnteredPip(
                                taskId = taskId,
            // If the task minimizing to PiP is the last task, modify wct to perform Desktop cleanup
            var desktopExitRunnable: RunOnTransitStart? = null
            if (isLastTask) {
                desktopExitRunnable =
                    performDesktopExitCleanUp(
                        wct = wct,
                        deskId = deskId,
                                displayId = taskInfo.displayId,
                                taskIsLastVisibleTaskBeforePip = isLastTask,
                            )
                        },
                    )
                        displayId = displayId,
                        willExitDesktop = true,
                    )
            }
            val transition = freeformTaskTransitionStarter.startPipTransition(wct)
            desktopExitRunnable?.invoke(transition)
        } else {
            snapEventHandler.removeTaskIfTiled(displayId, taskId)
            val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
@@ -1887,11 +1883,7 @@ class DesktopTasksController(
        displayId: Int,
        forceExitDesktop: Boolean,
    ): Boolean {
        if (
            forceExitDesktop &&
                (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
                    DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue)
        ) {
        if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            // |forceExitDesktop| is true when the callers knows we'll exit desktop, such as when
            // explicitly going fullscreen, so there's no point in checking the desktop state.
            return true
@@ -1908,33 +1900,6 @@ class DesktopTasksController(
        return true
    }

    /** Potentially perform Desktop cleanup after a task successfully enters PiP. */
    @VisibleForTesting
    fun onDesktopTaskEnteredPip(
        taskId: Int,
        deskId: Int,
        displayId: Int,
        taskIsLastVisibleTaskBeforePip: Boolean,
    ) {
        if (
            !willExitDesktop(taskId, displayId, forceExitDesktop = taskIsLastVisibleTaskBeforePip)
        ) {
            return
        }

        val wct = WindowContainerTransaction()
        val desktopExitRunnable =
            performDesktopExitCleanUp(
                wct = wct,
                deskId = deskId,
                displayId = displayId,
                willExitDesktop = true,
            )

        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
        desktopExitRunnable?.invoke(transition)
    }

    private fun performDesktopExitCleanupIfNeeded(
        taskId: Int,
        deskId: Int? = null,
@@ -1958,7 +1923,7 @@ class DesktopTasksController(
    }

    /** TODO: b/394268248 - update [deskId] to be non-null. */
    private fun performDesktopExitCleanUp(
    fun performDesktopExitCleanUp(
        wct: WindowContainerTransaction,
        deskId: Int?,
        displayId: Int,
@@ -3955,6 +3920,12 @@ class DesktopTasksController(
                DesktopTaskToFrontReason.TASKBAR_MANAGE_WINDOW ->
                    UnminimizeReason.TASKBAR_MANAGE_WINDOW
            }

        @JvmField
        /**
         * A placeholder for a synthetic transition that isn't backed by a true system transition.
         */
        val SYNTHETIC_TRANSITION: IBinder = Binder()
    }

    /** Defines interface for classes that can listen to changes for task resize. */
Loading