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

Commit 9e511e5f authored by Jorge Gil's avatar Jorge Gil
Browse files

Desks: Handle independent activation/deactivation edge cases

Increases handling of independent transitions involving desks or the
desktop wallpaper task to ensure WC order and DesktopRepository state
isn't left in a bad state (such as wallpaper over an active root, or
inactive desk in the repository for a desk that is in front)

1) A desk Changes to-back and to-front should trigger
   deactivation/activation respectively to ensure the launch root state
   is correct. This can happen during CTS runs when Home is brought to
   front as a mean to "reset" state between tests
2) A to-back desk Change of an active desk during user-switches should
   deactivate the launch root, but keep the desk active in the
   repository to restore to when switching back
3) A desktop wallpaper Change to-front and to-back should also trigger
   activation/deactivation respectively (with order changes) to ensure
   order is correct.
4) A desktop wallaper Change to-back during a user-switch should also
   deactivate the launch root, but keep the desk active in the
   repository to restore on switch back.
5) A combined wallpaper to-front and desk to-back could mean the last
   desk task was closed and WM is attempting to bring the next focusable
   task (wallpaper) to front. Ensure the desk is reorded to the front
   of the wallpaper in this case to preserve the empty desk.

Flag: com.android.window.flags.enable_multiple_desktops_backend
Fix: 416173119
Bug: 417045331
Bug: 418619628
Test: atest CtsWindowManagerDeviceWindow \
WindowMetricsActivityTests#testMetricsMatchesDisplayAreaOnFreeformActivity, \
testMetricsMatchesLayoutOnPipActivity
Test: switch users from one with an active desk (empty or not) to one
where we land in Home - check launching tasks defaults to fullscreen
(launch root was deactivated) and switching back restores the desk
Test: switch users from one with an active desk (empty or not) to one
with an active desk (empty or not) - verify desk is restored on switch
back
Test: close the last task in a desk - verify desk remains empty and
active

Change-Id: I8b5018863a4eba76f40e7747b02e9581ac9cbc7f
parent 4df088bd
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -1413,18 +1413,23 @@ public abstract class WMShellModule {
                                        desktopState,
                                        shellInit)));
    }

    @WMSingleton
    @Provides
    static Optional<DesksTransitionObserver> provideDesksTransitionObserver(
            @DynamicOverride DesktopUserRepositories desktopUserRepositories,
            @NonNull DesksOrganizer desksOrganizer,
            @NonNull Transitions transitions,
            @NonNull ShellController shellController,
            @NonNull DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider,
            @NonNull @ShellMainThread CoroutineScope mainScope,
            DesktopState desktopState
    ) {
        if (desktopState.canEnterDesktopModeOrShowAppHandle()) {
            return Optional.of(
                    new DesksTransitionObserver(desktopUserRepositories, desksOrganizer,
                            transitions));
                            transitions, shellController, desktopWallpaperActivityTokenProvider,
                            mainScope));
        }
        return Optional.empty();
    }
+269 −20
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@
 */
package com.android.wm.shell.desktopmode.multidesks

import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.os.IBinder
import android.view.Display.INVALID_DISPLAY
import android.view.WindowManager.TRANSIT_CHANGE
@@ -23,11 +24,21 @@ import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.DesktopExperienceFlags
import android.window.TransitionInfo
import android.window.TransitionInfo.FLAG_MOVED_TO_TOP
import android.window.WindowContainerTransaction
import com.android.app.tracing.traceSection
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.desktopmode.DesktopUserRepositories
import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.annotations.ShellMainThread
import com.android.wm.shell.sysui.ShellController
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION
import com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

/**
 * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It
@@ -37,6 +48,9 @@ class DesksTransitionObserver(
    private val desktopUserRepositories: DesktopUserRepositories,
    private val desksOrganizer: DesksOrganizer,
    private val transitions: Transitions,
    private val shellController: ShellController,
    private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider,
    @ShellMainThread private val mainScope: CoroutineScope,
) {
    private val deskTransitions = mutableMapOf<IBinder, MutableSet<DeskTransition>>()

@@ -53,13 +67,16 @@ class DesksTransitionObserver(
     * Called when any transition is ready, which may include transitions not tracked by this
     * observer.
     */
    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
    fun onTransitionReady(transition: IBinder, info: TransitionInfo) =
        traceSection("DesksTransitionObserver#onTransitionReady") {
            if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
            val deskTransitions = deskTransitions.remove(transition)
        deskTransitions?.forEach { deskTransition -> handleDeskTransition(info, deskTransition) }
        if (deskTransitions == null || deskTransitions.isEmpty()) {
            // A desk transition could also occur without shell having started it or intercepting
            // it, check for that here in case launch roots need to be updated.
            deskTransitions?.forEach { deskTransition ->
                handleDeskTransition(info, deskTransition)
            }
            if (deskTransitions.isNullOrEmpty()) {
                // A desk transition could also occur without shell having started it or
                // intercepting it, check for that here in case launch roots need to be updated.
                handleIndependentDeskTransitionIfNeeded(info)
            }
        }
@@ -231,23 +248,193 @@ class DesksTransitionObserver(
    }

    private fun handleIndependentDeskTransitionIfNeeded(info: TransitionInfo) {
        val deskChanges = info.changes.filter { c -> desksOrganizer.isDeskChange(c) }
        if (deskChanges.isEmpty()) return
        val deskChanges = info.deskChanges()
        val desktopWallpaperChanges = info.desktopWallpaperChanges()
        if (deskChanges.isEmpty() && desktopWallpaperChanges.isEmpty()) return
        logD(
            "handleIndependentDeskTransitionIfNeeded %d desk related changes found",
            "handleIndependentDeskTransitionIfNeeded %d desk related change(s) found with " +
                "%d desktop wallpaper change(s)",
            deskChanges.size,
            desktopWallpaperChanges.size,
        )
        if (info.isRecentsType()) {
            // Recents related changes are transient, so desk activation state should not change.
            logD("Desk changes in a recents transition, ignoring")
            return
        }
        val wct = WindowContainerTransaction()
        for (change in deskChanges.reversed()) {
            val deskId = desksOrganizer.getDeskIdFromChange(change) ?: continue
            when (change.mode) {
                TRANSIT_TO_BACK -> {
                    logD("handleIndependentDeskTransitionIfNeeded desk=%d moved to back", deskId)
        var hasSeenDesk = false
        val openingUserIds = mutableListOf<Int>()
        val desksToActivate = mutableListOf<Int>()
        // Visit all task changes, not just desk/wallpaper changes because we're interested in
        // capturing user ids of showing tasks (such as Home) to detect user switches.
        for (change in info.taskChanges()) {
            val taskInfo = checkNotNull(change.taskInfo) { "Expected non-null task info" }
            val taskId = taskInfo.taskId
            logD("Handle change for taskId=%d:", taskId)
            if (TransitionUtil.isOpeningMode(change.mode) || change.isToTop()) {
                logD("Opening/to-top change for userId=%d", taskInfo.userId)
                openingUserIds += taskInfo.userId
            }
            if (change !in deskChanges && change !in desktopWallpaperChanges) {
                logD("Not desk or wallpaper, skipping")
                continue
            }
            if (change in deskChanges) {
                hasSeenDesk = true
            }
            val changeUserId = taskInfo.userId
            val userSwitch = getUserSwitch(change, openingUserIds)
            logD(
                "Independent change userSwitch=%s changeUserId=%d openingUserIds=%s",
                userSwitch,
                changeUserId,
                openingUserIds,
            )

            if (change in desktopWallpaperChanges) {
                logD("Desktop wallpaper change")
                if (hasSeenDesk) {
                    logD("Saw desk change before desktop wallpaper change, skipping")
                    continue
                }
                when {
                    change.isToBack() -> {
                        // The desktop wallpaper is moving to back without seeing a desk first.
                        // This might mean an empty desk is moving to back, such as when having an
                        // empty desk active and switching users. Make sure the desk is deactivated.
                        // If this is a user switch, the userId of the desk change will have
                        // updated to the new user already, so use the old user id for reporting the
                        // deactivation to the going-away user's repository.
                        val userId = userSwitch?.oldUserId ?: changeUserId
                        val repository = desktopUserRepositories.getProfile(userId)
                        // When moving to back due to a user switch, keep the repository state as
                        // active for when the user session is restored.
                        val keepActiveInRepository = userSwitch != null
                        val activeDeskId = repository.getActiveDeskId(change.endDisplayId)
                        if (activeDeskId != null) {
                            logD(
                                "Desktop wallpaper of user=%d moved to back without visible desk" +
                                    ", will let the organizer deactivate " +
                                    "and the repository will %s",
                                userId,
                                if (keepActiveInRepository) "keep active" else "deactivate",
                            )
                            // Always let the organizer deactivate to clear the launch root.
                            desksOrganizer.deactivateDesk(wct, activeDeskId, skipReorder = true)
                            if (!keepActiveInRepository) {
                                // The desk was independently deactivated (such as when Home is
                                // brought to front during CTS), make sure the repository state
                                // reflects that too.
                                repository.setDeskInactive(activeDeskId)
                            }
                        } else {
                            logD(
                                "Desktop wallpaper of user=%d moved to back with no active desk" +
                                    ", skipping",
                                userId,
                            )
                        }
                    }
                    change.isToTop() -> {
                        // It is possible that the desktop wallpaper ends up on top of the desk.
                        // For example: when in an empty desktop, then switching users and back.
                        // WM restoration on user switches is only for visible root tasks, and an
                        // empty desk root task is technically invisible (even if top / "active"),
                        // so only the wallpaper is restored (in front of the desk).
                        // This isn't valid desktop state, so if a desk was supposed to be active
                        // activate it, otherwise dismiss the wallpaper.
                        val repository = desktopUserRepositories.getProfile(changeUserId)
                        val activeDeskId = repository.getActiveDeskId(change.endDisplayId)
                        logD(
                            "Found desktop wallpaper without a desk in front for userId=%d " +
                                "activeDeskId=%d",
                            changeUserId,
                            activeDeskId,
                        )
                        if (activeDeskId != null) {
                            logD("Reactivating desk=%d", activeDeskId)
                            desksToActivate.add(activeDeskId)
                            desksOrganizer.activateDesk(wct, activeDeskId, skipReorder = false)
                        } else {
                            logD("Dismissing desktop wallpaper")
                            val container =
                                checkNotNull(change.container) { "Expected non-null container" }
                            wct.reorder(container, /* onTop= */ false)
                        }
                    }
                    else -> {
                        logW(
                            "Unexpected change for desktop wallpaper with mode=%s",
                            TransitionInfo.modeToString(change.mode),
                        )
                    }
                }
                continue
            }

            val deskId = desksOrganizer.getDeskIdFromChange(change)
            if (deskId == null) {
                logW("No desk found in change")
                continue
            }
            val displayId = change.endDisplayId
            val usersWithDeskId =
                desktopUserRepositories.getRepositoriesWithDeskId(deskId).map { it.userId }
            logD(
                "Handle desk change for desk=%d in display=%d usersWithDeskId=%s",
                deskId,
                displayId,
                usersWithDeskId,
            )
            when {
                change.isToBack() -> {
                    if (
                        desksToActivate.contains(deskId) &&
                            DesktopExperienceFlags.ENABLE_EMPTY_DESK_ON_MINIMIZE.isTrue
                    ) {
                        // In cases such as back-navigating the last app, where we want to remain
                        // in an empty desk, WM core may forcefully move the desktop wallpaper to
                        // front and the desk root to back because the next focusable activity is
                        // the desktop wallpaper. This is another case of desktop wallpaper over
                        // desk that's already handled above so the desk is already scheduled to be
                        // reactivated. Just skip the deactivation here to avoid cancelling it out.
                        logD("desk=%d moved to back but is scheduled to activate, skipping", deskId)
                        continue
                    }
                    // If this is a user switch, the userId of the desk change will have updated to
                    // the new user already, so it can't be used for reporting the deactivation
                    // to the going-away user's repository.
                    val userId = userSwitch?.oldUserId ?: changeUserId
                    val repository = desktopUserRepositories.getProfile(userId)
                    // When moving to back due to a user switch, keep the repository state as
                    // active for when the user session is restored.
                    val keepActiveInRepository = userSwitch != null
                    logD(
                        "desk=%d of user=%d moved to back, will let the organizer deactivate and " +
                            "the repository will %s",
                        deskId,
                        userId,
                        if (keepActiveInRepository) "keep active" else "deactivate",
                    )
                    // Always let the organizer deactivate to clear the launch root.
                    desksOrganizer.deactivateDesk(wct, deskId, skipReorder = true)
                    if (!keepActiveInRepository) {
                        // The desk was independently deactivated (such as when Home is brought
                        // to front during CTS), make sure the repository state reflects that too.
                        repository.setDeskInactive(deskId)
                    }
                }
                TRANSIT_TO_FRONT -> {
                    logD("handleIndependentDeskTransitionIfNeeded desk=%d moved to front", deskId)
                change.isToTop() -> {
                    val repository = desktopUserRepositories.getProfile(changeUserId)
                    logD(
                        "desk=%d of user=%d moved to front, " +
                            "will let the organizer and repository activate",
                        deskId,
                        changeUserId,
                    )
                    desksOrganizer.activateDesk(wct, deskId, skipReorder = true)
                    repository.setActiveDesk(displayId, deskId)
                }
                else -> {
                    logW(
@@ -259,9 +446,71 @@ class DesksTransitionObserver(
            }
        }
        if (wct.isEmpty) {
            logD("No changes, ignoring")
            return
        }
        transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
        logD("handleIndependentDeskTransitionIfNeeded starting transition")
        mainScope.launch { transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) }
    }

    private fun getUserSwitch(
        change: TransitionInfo.Change,
        openingUserIds: List<Int>,
    ): UserSwitch? {
        val taskInfo = checkNotNull(change.taskInfo) { "Expected non-null task info" }
        val currentUserId = shellController.currentUserId
        val changeUserId = taskInfo.userId
        // This isn't a reliable way to identify user switches. It makes two assumptions:
        //  1) When switching from user A to user B with a desk active, the transition
        //  change for the desk is reported with the id of user B - that is because root
        //  tasks are not tied to a single user, so WM always sets the ID of the "current"
        //  (post-switch) user.
        //  2) The transition is handled by Shell before Shell knows about the user switch
        //  so |ShellController.userId| still reports user A.
        // TODO: b/418821815 - make desk ids unique even across users, or include user
        //  change info in the transition.
        val isUserSwitch =
            currentUserId != changeUserId ||
                // In some cases, like when switching users from one with an empty desk:
                //   1) there is no desk change because it was empty and thus already invisible
                //   2) the desktop wallpaper going to back has the userId of the new user (because
                //      it uses |showForAllUsers|.
                // So the only way to identify the user switch is by looking at other tasks (of
                // the new user) that are opening/to-top, such as Home if say the new user is
                // is restoring to Home.
                openingUserIds.filterNot { it == currentUserId }.isNotEmpty()
        if (!isUserSwitch) return null
        val newUserId =
            if (currentUserId != changeUserId) {
                changeUserId
            } else {
                openingUserIds.filterNot { it == currentUserId }.first()
            }
        return UserSwitch(oldUserId = currentUserId, newUserId = newUserId)
    }

    data class UserSwitch(val oldUserId: Int, val newUserId: Int)

    private fun TransitionInfo.isRecentsType() =
        type == TRANSIT_START_RECENTS_TRANSITION || type == TRANSIT_END_RECENTS_TRANSITION

    private fun TransitionInfo.Change.isToTop(): Boolean =
        (mode == TRANSIT_TO_FRONT) || hasFlags(FLAG_MOVED_TO_TOP)

    private fun TransitionInfo.Change.isToBack(): Boolean = mode == TRANSIT_TO_BACK

    private fun TransitionInfo.taskChanges(): List<TransitionInfo.Change> =
        changes.filter { c ->
            val taskId = c.taskInfo?.taskId
            taskId != null && taskId != INVALID_TASK_ID
        }

    private fun TransitionInfo.deskChanges(): List<TransitionInfo.Change> =
        changes.filter { c -> desksOrganizer.isDeskChange(c) }

    private fun TransitionInfo.desktopWallpaperChanges(): List<TransitionInfo.Change> =
        changes.filter { c ->
            c.container == desktopWallpaperActivityTokenProvider.getToken(c.endDisplayId)
        }

    private fun logD(msg: String, vararg arguments: Any?) {
+12 −2
Original line number Diff line number Diff line
@@ -514,7 +514,12 @@ class RootTaskDesksOrganizer(
        if (deskRootsByDeskId.contains(taskInfo.taskId)) {
            val deskId = taskInfo.taskId
            deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo)
            logV("Desk #$deskId's task info changed in display#${taskInfo.displayId}")
            logV(
                "Desk #$deskId's task info changed in display#%d visible=%b children=%s",
                taskInfo.displayId,
                taskInfo.isVisible,
                deskRootsByDeskId[deskId].children,
            )
            return
        }
        val minimizationRoot =
@@ -531,7 +536,12 @@ class RootTaskDesksOrganizer(
        if (parentTaskId in deskRootsByDeskId) {
            val deskId = taskInfo.parentTaskId
            val taskId = taskInfo.taskId
            logV("onTaskInfoChanged: Task #$taskId appeared in desk #$deskId")
            logV(
                "onTaskInfoChanged: Task #%d (visible=%b) appeared in desk #%d",
                taskId,
                taskInfo.isVisible,
                deskId,
            )
            addChildToDesk(taskId = taskId, deskId = deskId)
            return
        }
+1 −2
Original line number Diff line number Diff line
@@ -279,8 +279,7 @@ public class ShellController {
    }

    /** Returns the current user id. */
    @VisibleForTesting
    int getCurrentUserId() {
    public int getCurrentUserId() {
        return mUserId;
    }

+460 −37

File changed.

Preview size limit exceeded, changes collapsed.