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

Commit e77a1c00 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Desks: Handle independent activation/deactivation edge cases" into main

parents 570cc3f1 9e511e5f
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -1412,18 +1412,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.