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

Commit 148cfd94 authored by Merissa Mitchell's avatar Merissa Mitchell
Browse files

[PiP on Desktop] Minimizing last task to PiP should exit Desktop

Recall: http://recall/clips/fed3cb07-1e44-4ea8-9798-2689d29e0558

Previously, an active PiP window should keep us in Desktop mode even
when there are no other visible tasks. We are now reverting that
feature, so now we need to exit Desktop session if the task that is
entering PiP was the last visible task.

Also add a new DesktopPipTransitionObserver class to observe pending PiP
transitions entered from Desktop via minimize button.

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

Change-Id: Ifa59bbf0e055711c8d75bcb8163b4e16f6570d79
parent 3eef4944
Loading
Loading
Loading
Loading
+18 −0
Original line number Diff line number Diff line
@@ -98,6 +98,7 @@ 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;
@@ -778,6 +779,7 @@ public abstract class WMShellModule {
            OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver,
            DesksOrganizer desksOrganizer,
            Optional<DesksTransitionObserver> desksTransitionObserver,
            Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver,
            UserProfileContexts userProfileContexts,
            DesktopModeCompatPolicy desktopModeCompatPolicy,
            DragToDisplayTransitionHandler dragToDisplayTransitionHandler,
@@ -820,6 +822,7 @@ public abstract class WMShellModule {
                overviewToDesktopTransitionObserver,
                desksOrganizer,
                desksTransitionObserver.get(),
                desktopPipTransitionObserver.get(),
                userProfileContexts,
                desktopModeCompatPolicy,
                dragToDisplayTransitionHandler,
@@ -1211,6 +1214,7 @@ public abstract class WMShellModule {
            Transitions transitions,
            ShellTaskOrganizer shellTaskOrganizer,
            Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
            Optional<DesktopPipTransitionObserver> desktopPipTransitionObserver,
            Optional<BackAnimationController> backAnimationController,
            DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
            ShellInit shellInit) {
@@ -1223,6 +1227,7 @@ public abstract class WMShellModule {
                                        transitions,
                                        shellTaskOrganizer,
                                        desktopMixedTransitionHandler.get(),
                                        desktopPipTransitionObserver.get(),
                                        backAnimationController.get(),
                                        desktopWallpaperActivityTokenProvider,
                                        shellInit)));
@@ -1242,6 +1247,19 @@ 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(
+81 −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.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"
    }
}
+0 −58
Original line number Diff line number Diff line
@@ -68,7 +68,6 @@ class DesktopRepository(
     * @property topTransparentFullscreenTaskId the task id of any current top transparent
     *   fullscreen task launched on top of the desk. Cleared when the transparent task is closed or
     *   sent to back. (top is at index 0).
     * @property pipTaskId the task id of PiP task entered while in Desktop Mode.
     */
    private data class Desk(
        val deskId: Int,
@@ -81,7 +80,6 @@ class DesktopRepository(
        val freeformTasksInZOrder: ArrayList<Int> = ArrayList(),
        var fullImmersiveTaskId: Int? = null,
        var topTransparentFullscreenTaskId: Int? = null,
        var pipTaskId: Int? = null,
    ) {
        fun deepCopy(): Desk =
            Desk(
@@ -94,7 +92,6 @@ class DesktopRepository(
                freeformTasksInZOrder = ArrayList(freeformTasksInZOrder),
                fullImmersiveTaskId = fullImmersiveTaskId,
                topTransparentFullscreenTaskId = topTransparentFullscreenTaskId,
                pipTaskId = pipTaskId,
            )

        // TODO: b/362720497 - remove when multi-desktops is enabled where instances aren't
@@ -107,7 +104,6 @@ class DesktopRepository(
            freeformTasksInZOrder.clear()
            fullImmersiveTaskId = null
            topTransparentFullscreenTaskId = null
            pipTaskId = null
        }
    }

@@ -127,9 +123,6 @@ class DesktopRepository(
    /* Tracks last bounds of task before toggled to immersive state. */
    private val boundsBeforeFullImmersiveByTaskId = SparseArray<Rect>()

    /* Callback for when a pending PiP transition has been aborted. */
    private var onPipAbortedCallback: ((Int, Int) -> Unit)? = null

    private var desktopGestureExclusionListener: Consumer<Region>? = null
    private var desktopGestureExclusionExecutor: Executor? = null

@@ -610,57 +603,6 @@ class DesktopRepository(
        }
    }

    /**
     * Set whether the given task is the Desktop-entered PiP task in this display's active desk.
     *
     * TODO: b/389960283 - add explicit [deskId] argument.
     */
    fun setTaskInPip(displayId: Int, taskId: Int, enterPip: Boolean) {
        val activeDesk =
            desktopData.getActiveDesk(displayId)
                ?: error("Expected active desk in display: $displayId")
        if (enterPip) {
            activeDesk.pipTaskId = taskId
        } else {
            activeDesk.pipTaskId =
                if (activeDesk.pipTaskId == taskId) null
                else {
                    logW(
                        "setTaskInPip: taskId=%d did not match saved taskId=%d",
                        taskId,
                        activeDesk.pipTaskId,
                    )
                    activeDesk.pipTaskId
                }
        }
    }

    /**
     * Returns whether the given task is the Desktop-entered PiP task in this display's active desk.
     *
     * TODO: b/389960283 - add explicit [deskId] argument.
     */
    fun isTaskMinimizedPipInDisplay(displayId: Int, taskId: Int): Boolean =
        desktopData.getActiveDesk(displayId)?.pipTaskId == taskId

    /**
     * Saves callback to handle a pending PiP transition being aborted.
     *
     * TODO: b/389960283 - add explicit [deskId] argument.
     */
    fun setOnPipAbortedCallback(callbackIfPipAborted: ((displayId: Int, pipTaskId: Int) -> Unit)?) {
        onPipAbortedCallback = callbackIfPipAborted
    }

    /**
     * Invokes callback to handle a pending PiP transition with the given task id being aborted.
     *
     * TODO: b/389960283 - add explicit [deskId] argument.
     */
    fun onPipAborted(displayId: Int, pipTaskId: Int) {
        onPipAbortedCallback?.invoke(displayId, pipTaskId)
    }

    /**
     * Set whether the given task is the full-immersive task in this display's active desk.
     *
+100 −63
Original line number Diff line number Diff line
@@ -208,6 +208,7 @@ class DesktopTasksController(
    private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver,
    private val desksOrganizer: DesksOrganizer,
    private val desksTransitionObserver: DesksTransitionObserver,
    private val desktopPipTransitionObserver: DesktopPipTransitionObserver,
    private val userProfileContexts: UserProfileContexts,
    private val desktopModeCompatPolicy: DesktopModeCompatPolicy,
    private val dragToDisplayTransitionHandler: DragToDisplayTransitionHandler,
@@ -764,10 +765,30 @@ class DesktopTasksController(

    fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
        val wct = WindowContainerTransaction()

        val taskId = taskInfo.taskId
        val displayId = taskInfo.displayId
        val deskId =
            taskRepository.getDeskIdForTask(taskInfo.taskId)
                ?: if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                    logW("minimizeTask: desk not found for task: ${taskInfo.taskId}")
                    return
                } else {
                    getDefaultDeskId(taskInfo.displayId)
                }
        val isLastTask =
            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                taskRepository.isOnlyVisibleNonClosingTaskInDesk(
                    taskId = taskId,
                    deskId = checkNotNull(deskId) { "Expected non-null deskId" },
                    displayId = displayId,
                )
            } else {
                taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
            }
        val isMinimizingToPip =
            DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
                (taskInfo.pictureInPictureParams?.isAutoEnterEnabled() ?: false)
                (taskInfo.pictureInPictureParams?.isAutoEnterEnabled ?: false)

        // If task is going to PiP, start a PiP transition instead of a minimize transition
        if (isMinimizingToPip) {
            val requestInfo =
@@ -781,28 +802,22 @@ class DesktopTasksController(
                )
            val requestRes = transitions.dispatchRequest(Binder(), requestInfo, /* skip= */ null)
            wct.merge(requestRes.second, true)
            freeformTaskTransitionStarter.startPipTransition(wct)
            taskRepository.setTaskInPip(taskInfo.displayId, taskInfo.taskId, enterPip = true)
            taskRepository.setOnPipAbortedCallback { displayId, taskId ->
                minimizeTaskInner(shellTaskOrganizer.getRunningTaskInfo(taskId)!!, minimizeReason)
                taskRepository.setTaskInPip(displayId, taskId, enterPip = false)
            }
            return
        }

        minimizeTaskInner(taskInfo, minimizeReason)
    }

    private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
        val taskId = taskInfo.taskId
        val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
        if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            logW("minimizeTaskInner: desk not found for task: ${taskInfo.taskId}")
            return
        }
        val displayId = taskInfo.displayId
        val wct = WindowContainerTransaction()

            desktopPipTransitionObserver.addPendingPipTransition(
                DesktopPipTransitionObserver.PendingPipTransition(
                    token = freeformTaskTransitionStarter.startPipTransition(wct),
                    taskId = taskInfo.taskId,
                    onSuccess = {
                        onDesktopTaskEnteredPip(
                            taskId = taskId,
                            deskId = deskId,
                            displayId = taskInfo.displayId,
                            taskIsLastVisibleTaskBeforePip = isLastTask,
                        )
                    },
                )
            )
        } else {
            snapEventHandler.removeTaskIfTiled(displayId, taskId)
            val willExitDesktop = willExitDesktop(taskId, displayId, forceExitDesktop = false)
            val desktopExitRunnable =
@@ -828,16 +843,6 @@ class DesktopTasksController(
            } else {
                wct.reorder(taskInfo.token, /* onTop= */ false)
            }
        val isLastTask =
            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                taskRepository.isOnlyVisibleNonClosingTaskInDesk(
                    taskId = taskId,
                    deskId = checkNotNull(deskId) { "Expected non-null deskId" },
                    displayId = displayId,
                )
            } else {
                taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
            }
            val transition =
                freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
            desktopTasksLimiter.ifPresent {
@@ -851,6 +856,7 @@ class DesktopTasksController(
            exitResult.asExit()?.runOnTransitionStart?.invoke(transition)
            desktopExitRunnable?.invoke(transition)
        }
    }

    /** Move a task with given `taskId` to fullscreen */
    fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) {
@@ -1806,7 +1812,11 @@ class DesktopTasksController(
        displayId: Int,
        forceExitDesktop: Boolean,
    ): Boolean {
        if (forceExitDesktop && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
        if (
            forceExitDesktop &&
                (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue ||
                    DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.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
@@ -1823,6 +1833,33 @@ 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,
+2 −44
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
@@ -41,8 +40,6 @@ import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP
import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP

/**
 * A [Transitions.TransitionObserver] that observes shell transitions and updates the
@@ -55,6 +52,7 @@ class DesktopTasksTransitionObserver(
    private val transitions: Transitions,
    private val shellTaskOrganizer: ShellTaskOrganizer,
    private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
    private val desktopPipTransitionObserver: DesktopPipTransitionObserver,
    private val backAnimationController: BackAnimationController,
    private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
    shellInit: ShellInit,
@@ -63,8 +61,6 @@ class DesktopTasksTransitionObserver(
    data class CloseWallpaperTransition(val transition: IBinder, val displayId: Int)

    private var transitionToCloseWallpaper: CloseWallpaperTransition? = null
    /* Pending PiP transition and its associated display id and task id. */
    private var pendingPipTransitionAndPipTask: Triple<IBinder, Int, Int>? = null
    private var currentProfileId: Int

    init {
@@ -98,33 +94,7 @@ class DesktopTasksTransitionObserver(
            removeTaskIfNeeded(info)
        }
        removeWallpaperOnLastTaskClosingIfNeeded(transition, info)

        val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
        info.changes.forEach { change ->
            change.taskInfo?.let { taskInfo ->
                if (
                    DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PIP.isTrue &&
                        desktopRepository.isTaskMinimizedPipInDisplay(
                            taskInfo.displayId,
                            taskInfo.taskId,
                        )
                ) {
                    when (info.type) {
                        TRANSIT_PIP ->
                            pendingPipTransitionAndPipTask =
                                Triple(transition, taskInfo.displayId, taskInfo.taskId)

                        TRANSIT_EXIT_PIP,
                        TRANSIT_REMOVE_PIP ->
                            desktopRepository.setTaskInPip(
                                taskInfo.displayId,
                                taskInfo.taskId,
                                enterPip = false,
                            )
                    }
                }
            }
        }
        desktopPipTransitionObserver.onTransitionReady(transition, info)
    }

    private fun removeTaskIfNeeded(info: TransitionInfo) {
@@ -299,18 +269,6 @@ class DesktopTasksTransitionObserver(
                    }
                }
            transitionToCloseWallpaper = null
        } else if (pendingPipTransitionAndPipTask?.first == transition) {
            val desktopRepository = desktopUserRepositories.getProfile(currentProfileId)
            if (aborted) {
                pendingPipTransitionAndPipTask?.let {
                    desktopRepository.onPipAborted(
                        /*displayId=*/ it.second,
                        /* taskId=*/ it.third,
                    )
                }
            }
            desktopRepository.setOnPipAbortedCallback(null)
            pendingPipTransitionAndPipTask = null
        }
    }

Loading