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

Commit f4d9f483 authored by Jorge Gil's avatar Jorge Gil
Browse files

Desks: Add multi-user support

Implements full multi-user support when multiple-desks are enabled.
Prior to this change, desk roots where only created for the current
user, and attempts to create them for another user would fail, resulting
in a persisted desk not being restored on reboot, or a default desk not
being created at all.

This change introduces a |userId| argument to DesksOrganizer's
create/remove functions, to allow implementations to manage roots
per-user. Since root-tasks are user-agnostic in WMCore,
RootTasksDesksOrganizer doesn't need to create different roots for N
desks times M users, and can instead reuse one root for one desk of each
user. DeskRoot#users is added to keep track of which users are already
using a given root to known when a new root actually needs to be
created.

Flag: com.android.window.flags.enable_multiple_desktops_backend
Bug: 403685486
Bug: 400984250
Bug: 399394443
Test: create multiple desks, switch users and create multiple desks -
verify roots are reused when possible and new roots are created when
needed. Also verify removing a user's desk does not remove the root
unless it is not used by any other users.
Test: create multiple desks under multiple users, reboot - verify
DesktopRepository restores all user desktops
Test: In an HSUM build, create multiple desks, reboot, try to enter
desktop mode - verify no crash

Change-Id: I7d6bd3cab235bebdaa94e01f2f102d4c494f7857
parent da47f1d5
Loading
Loading
Loading
Loading
+9 −13
Original line number Diff line number Diff line
@@ -49,9 +49,6 @@ class DesktopDisplayEventHandler(
    private val desktopDisplayModeController: DesktopDisplayModeController,
) : OnDisplaysChangedListener, OnDeskRemovedListener {

    private val desktopRepository: DesktopRepository
        get() = desktopUserRepositories.current

    init {
        shellInit.addInitCallback({ onInit() }, this)
    }
@@ -66,7 +63,7 @@ class DesktopDisplayEventHandler(
                object : UserChangeListener {
                    override fun onUserChanged(newUserId: Int, userContext: Context) {
                        val displayIds = rootTaskDisplayAreaOrganizer.displayIds
                        createDefaultDesksIfNeeded(displayIds.toSet())
                        createDefaultDesksIfNeeded(displayIds.toSet(), newUserId)
                    }
                }
            )
@@ -78,7 +75,7 @@ class DesktopDisplayEventHandler(
            desktopDisplayModeController.refreshDisplayWindowingMode()
        }

        createDefaultDesksIfNeeded(displayIds = setOf(displayId))
        createDefaultDesksIfNeeded(displayIds = setOf(displayId), userId = null)
    }

    override fun onDisplayRemoved(displayId: Int) {
@@ -99,23 +96,22 @@ class DesktopDisplayEventHandler(
    }

    override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {
        val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId)
        if (remainingDesks == 0) {
            logV("All desks removed from display#$lastDisplayId")
            createDefaultDesksIfNeeded(setOf(lastDisplayId))
        }
        createDefaultDesksIfNeeded(setOf(lastDisplayId), userId = null)
    }

    private fun createDefaultDesksIfNeeded(displayIds: Set<Int>) {
    private fun createDefaultDesksIfNeeded(displayIds: Set<Int>, userId: Int?) {
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
        logV("createDefaultDesksIfNeeded displays=%s", displayIds)
        mainScope.launch {
            desktopRepositoryInitializer.isInitialized.collect { initialized ->
                if (!initialized) return@collect
                val repository =
                    userId?.let { desktopUserRepositories.getProfile(userId) }
                        ?: desktopUserRepositories.current
                displayIds
                    .filter { displayId -> displayId != Display.INVALID_DISPLAY }
                    .filter { displayId -> supportsDesks(displayId) }
                    .filter { displayId -> desktopRepository.getNumberOfDesks(displayId) == 0 }
                    .filter { displayId -> repository.getNumberOfDesks(displayId) == 0 }
                    .also { displaysNeedingDesk ->
                        logV(
                            "createDefaultDesksIfNeeded creating default desks in displays=%s",
@@ -125,7 +121,7 @@ class DesktopDisplayEventHandler(
                    .forEach { displayId ->
                        // TODO: b/393978539 - consider activating the desk on creation when
                        //  applicable, such as for connected displays.
                        desktopTasksController.createDesk(displayId)
                        desktopTasksController.createDesk(displayId, repository.userId)
                    }
                cancel()
            }
+55 −28
Original line number Diff line number Diff line
@@ -42,6 +42,7 @@ import android.os.Handler
import android.os.IBinder
import android.os.SystemProperties
import android.os.UserHandle
import android.os.UserManager
import android.util.Slog
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
@@ -115,7 +116,6 @@ import com.android.wm.shell.desktopmode.multidesks.DeskTransition
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener
import com.android.wm.shell.desktopmode.multidesks.createDesk
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer
import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer.DeskRecreationFactory
import com.android.wm.shell.draganddrop.DragAndDropController
@@ -163,6 +163,7 @@ import java.util.Optional
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import kotlin.coroutines.suspendCoroutine
import kotlin.jvm.optionals.getOrNull

/**
@@ -283,14 +284,8 @@ class DesktopTasksController(

        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            desktopRepositoryInitializer.deskRecreationFactory =
                DeskRecreationFactory { deskUserId, destinationDisplayId, deskId ->
                    if (deskUserId != userId) {
                        // TODO: b/400984250 - add multi-user support for multi-desk restoration.
                        logW("Tried to re-create desk of another user.")
                        null
                    } else {
                        desksOrganizer.createDesk(destinationDisplayId)
                    }
                DeskRecreationFactory { deskUserId, destinationDisplayId, _ ->
                    createDeskSuspending(displayId = destinationDisplayId, userId = deskUserId)
                }
        }
    }
@@ -493,20 +488,53 @@ class DesktopTasksController(
        runOnTransitStart?.invoke(transition)
    }

    /** Creates a new desk in the given display. */
    fun createDesk(displayId: Int) {
    /** Adds a new desk to the given display for the given user. */
    fun createDesk(displayId: Int, userId: Int = this.userId) {
        logV("addDesk displayId=%d, userId=%d", displayId, userId)
        val repository = userRepositories.getProfile(userId)
        createDesk(displayId, userId) { deskId ->
            if (deskId == null) {
                logW("Failed to add desk in displayId=%d for userId=%d", displayId, userId)
            } else {
                repository.addDesk(displayId = displayId, deskId = deskId)
            }
        }
    }

    private fun createDesk(displayId: Int, userId: Int = this.userId, onResult: (Int?) -> Unit) {
        if (displayId == Display.INVALID_DISPLAY) {
            logW("createDesk attempt with invalid displayId", displayId)
            onResult(null)
            return
        }
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            desksOrganizer.createDesk(displayId) { deskId ->
                taskRepository.addDesk(displayId = displayId, deskId = deskId)
            }
        } else {
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            // In single-desk, the desk reuses the display id.
            taskRepository.addDesk(displayId = displayId, deskId = displayId)
            logD("createDesk reusing displayId=%d for single-desk", displayId)
            onResult(displayId)
            return
        }
        if (
            DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue &&
                UserManager.isHeadlessSystemUserMode() &&
                UserHandle.USER_SYSTEM == userId
        ) {
            logW("createDesk ignoring attempt for system user")
            return
        }
        desksOrganizer.createDesk(displayId, userId) { deskId ->
            logD(
                "createDesk obtained deskId=%d for displayId=%d and userId=%d",
                deskId,
                displayId,
                userId,
            )
            onResult(deskId)
        }
    }

    private suspend fun createDeskSuspending(displayId: Int, userId: Int = this.userId): Int? =
        suspendCoroutine { cont ->
            createDesk(displayId, userId) { deskId -> cont.resumeWith(Result.success(deskId)) }
        }

    /** Moves task to desktop mode if task is running, else launches it in desktop mode. */
@@ -3024,8 +3052,8 @@ class DesktopTasksController(
            }

        val wct = WindowContainerTransaction()
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
        tasksToRemove.forEach {
            // TODO: b/404595635 - consider moving this block into [DesksOrganizer].
            val task = shellTaskOrganizer.getRunningTaskInfo(it)
            if (task != null) {
                wct.removeTask(task.token)
@@ -3033,9 +3061,8 @@ class DesktopTasksController(
                recentTasksController?.removeBackgroundTask(it)
            }
        }
        } else {
            // TODO: 362720497 - double check background tasks are also removed.
            desksOrganizer.removeDesk(wct, deskId)
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            desksOrganizer.removeDesk(wct, deskId, userId)
        }
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return
        val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null)
+4 −12
Original line number Diff line number Diff line
@@ -18,13 +18,11 @@ package com.android.wm.shell.desktopmode.multidesks
import android.app.ActivityManager
import android.window.TransitionInfo
import android.window.WindowContainerTransaction
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback
import kotlin.coroutines.suspendCoroutine

/** An organizer of desk containers in which to host child desktop windows. */
interface DesksOrganizer {
    /** Creates a new desk container in the given display. */
    fun createDesk(displayId: Int, callback: OnCreateCallback)
    /** Creates a new desk container to use in the given display for the given user. */
    fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback)

    /** Activates the given desk, making it visible in its display. */
    fun activateDesk(wct: WindowContainerTransaction, deskId: Int)
@@ -32,8 +30,8 @@ interface DesksOrganizer {
    /** Deactivates the given desk, removing it as the default launch container for new tasks. */
    fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int)

    /** Removes the given desk and its desktop windows. */
    fun removeDesk(wct: WindowContainerTransaction, deskId: Int)
    /** Removes the given desk of the given user. */
    fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int)

    /** Moves the given task to the given desk. */
    fun moveTaskToDesk(
@@ -87,9 +85,3 @@ interface DesksOrganizer {
        fun onCreated(deskId: Int)
    }
}

/** Creates a new desk container in the given display. */
suspend fun DesksOrganizer.createDesk(displayId: Int): Int = suspendCoroutine { cont ->
    val onCreateCallback = OnCreateCallback { deskId -> cont.resumeWith(Result.success(deskId)) }
    createDesk(displayId, onCreateCallback)
}
+83 −19
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.window.TransitionInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.core.util.forEach
import androidx.core.util.valueIterator
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.ShellTaskOrganizer
@@ -40,7 +41,13 @@ import com.android.wm.shell.sysui.ShellCommandHandler
import com.android.wm.shell.sysui.ShellInit
import java.io.PrintWriter

/** A [DesksOrganizer] that uses root tasks as the container of each desk. */
/**
 * A [DesksOrganizer] that uses root tasks as the container of each desk.
 *
 * Note that root tasks are reusable between multiple users at the same time, and may also be
 * pre-created to have one ready for the first entry to the default desk, so root-task existence
 * does not imply a formal desk exists to the user.
 */
class RootTaskDesksOrganizer(
    shellInit: ShellInit,
    shellCommandHandler: ShellCommandHandler,
@@ -65,9 +72,26 @@ class RootTaskDesksOrganizer(
        }
    }

    override fun createDesk(displayId: Int, callback: OnCreateCallback) {
        logV("createDesk in display: %d", displayId)
        createDeskRootRequests += CreateDeskRequest(displayId, callback)
    override fun createDesk(displayId: Int, userId: Int, callback: OnCreateCallback) {
        logV("createDesk in displayId=%d userId=%s", displayId, userId)
        // Find an existing desk that is not yet used by this user.
        val unassignedDesk =
            deskRootsByDeskId
                .valueIterator()
                .asSequence()
                .filterNot { desk -> userId in desk.users }
                .firstOrNull()
        if (unassignedDesk != null) {
            unassignedDesk.users.add(userId)
            callback.onCreated(unassignedDesk.deskId)
            return
        }
        createDeskRoot(displayId, userId, callback)
    }

    private fun createDeskRoot(displayId: Int, userId: Int, callback: OnCreateCallback) {
        logV("createDeskRoot in display: %d for user: %d", displayId, userId)
        createDeskRootRequests += CreateDeskRequest(displayId, userId, callback)
        shellTaskOrganizer.createRootTask(
            displayId,
            WINDOWING_MODE_FREEFORM,
@@ -76,32 +100,53 @@ class RootTaskDesksOrganizer(
        )
    }

    override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) {
        logV("removeDesk %d", deskId)
        deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
    override fun removeDesk(wct: WindowContainerTransaction, deskId: Int, userId: Int) {
        logV("removeDesk %d for userId=%d", deskId, userId)
        val deskRoot = deskRootsByDeskId[deskId]
        if (deskRoot == null) {
            logW("removeDesk attempted to remove non-existent desk=%d", deskId)
            return
        }
        updateLaunchRoot(wct, deskId, enabled = false)
        deskRoot.users.remove(userId)
        if (deskRoot.users.isEmpty()) {
            // No longer in use by any users, remove it completely.
            logD("removeDesk %d is no longer used by any users, removing it completely", deskId)
            wct.removeRootTask(deskRoot.token)
            deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
        }
    }

    override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) {
        logV("activateDesk %d", deskId)
        val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
        wct.reorder(root.token, /* onTop= */ true)
        wct.setLaunchRoot(
            /* container= */ root.taskInfo.token,
            /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
            /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD),
        )
        updateLaunchRoot(wct, deskId, enabled = true)
    }

    override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) {
        logV("deactivateDesk %d", deskId)
        updateLaunchRoot(wct, deskId, enabled = false)
    }

    private fun updateLaunchRoot(wct: WindowContainerTransaction, deskId: Int, enabled: Boolean) {
        val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
        root.isLaunchRootRequested = enabled
        logD("updateLaunchRoot deskId=%d enabled=%b", deskId, enabled)
        if (enabled) {
            wct.setLaunchRoot(
                /* container= */ root.taskInfo.token,
                /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
                /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD),
            )
        } else {
            wct.setLaunchRoot(
                /* container= */ root.taskInfo.token,
                /* windowingModes= */ null,
                /* activityTypes= */ null,
            )
        }
    }

    override fun moveTaskToDesk(
        wct: WindowContainerTransaction,
@@ -275,7 +320,13 @@ class RootTaskDesksOrganizer(
            // Appearing root matches desk request.
            val deskId = taskInfo.taskId
            logV("Desk #$deskId appeared")
            deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash)
            deskRootsByDeskId[deskId] =
                DeskRoot(
                    deskId = deskId,
                    taskInfo = taskInfo,
                    leash = leash,
                    users = mutableSetOf(deskRequest.userId),
                )
            createDeskRootRequests.remove(deskRequest)
            deskRequest.onCreateCallback.onCreated(deskId)
            createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId)
@@ -430,6 +481,8 @@ class RootTaskDesksOrganizer(
        val taskInfo: RunningTaskInfo,
        val leash: SurfaceControl,
        val children: MutableSet<Int> = mutableSetOf(),
        val users: MutableSet<Int> = mutableSetOf(),
        var isLaunchRootRequested: Boolean = false,
    ) {
        val token: WindowContainerToken = taskInfo.token
    }
@@ -449,15 +502,24 @@ class RootTaskDesksOrganizer(

    private data class CreateDeskRequest(
        val displayId: Int,
        val userId: Int,
        val onCreateCallback: OnCreateCallback,
    )

    private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int)

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

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

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

    private fun logE(msg: String, vararg arguments: Any?) {
        ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }
@@ -473,7 +535,9 @@ class RootTaskDesksOrganizer(
            val minimizationRoot = deskMinimizationRootsByDeskId[deskId]
            pw.println("$innerPrefix  #$deskId visible=${root.taskInfo.isVisible}")
            pw.println("$innerPrefix    displayId=${root.taskInfo.displayId}")
            pw.println("$innerPrefix    isLaunchRootRequested=${root.isLaunchRootRequested}")
            pw.println("$innerPrefix    children=${root.children}")
            pw.println("$innerPrefix    users=${root.users}")
            pw.println("$innerPrefix    minimization root:")
            pw.println("$innerPrefix      rootId=${minimizationRoot?.rootId}")
            if (minimizationRoot != null) {
+8 −4
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness

@@ -213,12 +214,15 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun testUserChanged_createsDeskWhenNeeded() =
        testScope.runTest {
            val userId = 11
            whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true)
            val userChangeListenerCaptor = argumentCaptor<UserChangeListener>()
            verify(mockShellController).addUserChangeListener(userChangeListenerCaptor.capture())
            whenever(mockDesktopRepository.getNumberOfDesks(displayId = 2)).thenReturn(0)
            whenever(mockDesktopRepository.getNumberOfDesks(displayId = 3)).thenReturn(0)
            whenever(mockDesktopRepository.getNumberOfDesks(displayId = 4)).thenReturn(1)
            val mockRepository = mock<DesktopRepository>()
            whenever(mockDesktopUserRepositories.getProfile(userId)).thenReturn(mockRepository)
            whenever(mockRepository.getNumberOfDesks(displayId = 2)).thenReturn(0)
            whenever(mockRepository.getNumberOfDesks(displayId = 3)).thenReturn(0)
            whenever(mockRepository.getNumberOfDesks(displayId = 4)).thenReturn(1)
            whenever(mockRootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(2, 3, 4))
            desktopRepositoryInitializer.initialize(mockDesktopUserRepositories)
            handler.onDisplayAdded(displayId = 2)
@@ -227,7 +231,7 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() {
            runCurrent()

            clearInvocations(mockDesktopTasksController)
            userChangeListenerCaptor.lastValue.onUserChanged(1, context)
            userChangeListenerCaptor.lastValue.onUserChanged(userId, context)
            runCurrent()

            verify(mockDesktopTasksController).createDesk(displayId = 2)
Loading