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

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

Merge "Desks: Create desk (desktop-first) or pre-create root (touch-first)" into main

parents 9fd316af 6b7b7af2
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -102,6 +102,14 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer {
        }
    }

    /** Unregisters the given listener associated to the given display. */
    public void unregisterListener(int displayId, RootTaskDisplayAreaListener listener) {
        final ArrayList<RootTaskDisplayAreaListener> listeners = mListeners.get(displayId);
        if (listeners != null) {
            listeners.remove(listener);
        }
    }

    public void unregisterListener(RootTaskDisplayAreaListener listener) {
        for (int i = mListeners.size() - 1; i >= 0; --i) {
            final List<RootTaskDisplayAreaListener> listeners = mListeners.valueAt(i);
+60 −17
Original line number Diff line number Diff line
@@ -17,15 +17,21 @@
package com.android.wm.shell.desktopmode

import android.content.Context
import android.os.UserHandle
import android.os.UserManager
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
import android.window.DesktopExperienceFlags
import android.window.DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_ACTIVATION_IN_DESKTOP_FIRST_DISPLAYS
import android.window.DesktopModeFlags
import android.window.DisplayAreaInfo
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.RootTaskDisplayAreaOrganizer
import com.android.wm.shell.RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener
import com.android.wm.shell.common.DisplayController
import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener
import com.android.wm.shell.desktopmode.desktopfirst.DesktopDisplayModeController
import com.android.wm.shell.desktopmode.desktopfirst.isDisplayDesktopFirst
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver
import com.android.wm.shell.desktopmode.multidesks.OnDeskDisplayChangeListener
@@ -56,6 +62,11 @@ class DesktopDisplayEventHandler(
    private val desktopState: DesktopState,
) : OnDisplaysChangedListener, OnDeskRemovedListener, OnDeskDisplayChangeListener {

    private val onDisplayAreaChangeListener = OnDisplayAreaChangeListener { displayId ->
        logV("displayAreaChanged in displayId=%d", displayId)
        createDefaultDesksIfNeeded(displayIds = listOf(displayId), userId = null)
    }

    init {
        shellInit.addInitCallback({ onInit() }, this)
    }
@@ -65,12 +76,12 @@ class DesktopDisplayEventHandler(

        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            desktopTasksController.onDeskRemovedListener = this

            shellController.addUserChangeListener(
                object : UserChangeListener {
                    override fun onUserChanged(newUserId: Int, userContext: Context) {
                        val displayIds = rootTaskDisplayAreaOrganizer.displayIds
                        createDefaultDesksIfNeeded(displayIds.toSet(), newUserId)
                        val displayIds = rootTaskDisplayAreaOrganizer.displayIds.toSet()
                        logV("onUserChanged newUserId=%d displays=%s", newUserId, displayIds)
                        createDefaultDesksIfNeeded(displayIds, newUserId)
                    }
                }
            )
@@ -82,17 +93,21 @@ class DesktopDisplayEventHandler(
    }

    override fun onDisplayAdded(displayId: Int) {
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            rootTaskDisplayAreaOrganizer.registerListener(displayId, onDisplayAreaChangeListener)
        }
        if (displayId != DEFAULT_DISPLAY) {
            desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId)
            // The default display's windowing mode depends on the availability of the external
            // display. So updating the default display's windowing mode here.
            desktopDisplayModeController.updateDefaultDisplayWindowingMode()
        }

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

    override fun onDisplayRemoved(displayId: Int) {
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            rootTaskDisplayAreaOrganizer.unregisterListener(displayId, onDisplayAreaChangeListener)
        }
        if (displayId != DEFAULT_DISPLAY) {
            desktopDisplayModeController.updateDefaultDisplayWindowingMode()
        }
@@ -112,25 +127,39 @@ class DesktopDisplayEventHandler(
    }

    override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) {
        createDefaultDesksIfNeeded(setOf(lastDisplayId), userId = null)
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
        logV("onDeskRemoved deskId=%d displayId=%d", deskId, lastDisplayId)
        createDefaultDesksIfNeeded(listOf(lastDisplayId), userId = null)
    }

    private fun createDefaultDesksIfNeeded(displayIds: Set<Int>, userId: Int?) {
    private fun createDefaultDesksIfNeeded(displayIds: Collection<Int>, userId: Int?) {
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
        logV("createDefaultDesksIfNeeded displays=%s", displayIds)
        logV("createDefaultDesksIfNeeded displays=%s userId=%d", displayIds, userId)
        if (userId != null && !isUserDesktopEligible(userId)) {
            logW("createDefaultDesksIfNeeded ignoring attempt for ineligible user")
            return
        }
        mainScope.launch {
            desktopRepositoryInitializer.isInitialized.collect { initialized ->
                if (!initialized) return@collect
                val repository =
                    userId?.let { desktopUserRepositories.getProfile(userId) }
                        ?: desktopUserRepositories.current
                if (!isUserDesktopEligible(repository.userId)) {
                    logW("createDefaultDesksIfNeeded ignoring attempt for ineligible user")
                    cancel()
                    return@collect
                }
                for (displayId in displayIds) {
                    if (!shouldCreateOrWarmUpDesk(displayId, repository)) continue
                    if (isDisplayDesktopFirst(displayId)) {
                    if (rootTaskDisplayAreaOrganizer.isDisplayDesktopFirst(displayId)) {
                        logV("Display %d is desktop-first and needs a default desk", displayId)
                        desktopTasksController.createDesk(
                            displayId = displayId,
                            userId = repository.userId,
                            // TODO: b/393978539 - do not activate as a result of removing the
                            //  last desk from Overview. Let overview activate it once it is
                            //  selected or when the user goes home.
                            activateDesk =
                                ENABLE_MULTIPLE_DESKTOPS_ACTIVATION_IN_DESKTOP_FIRST_DISPLAYS.isTrue,
                        )
@@ -156,7 +185,7 @@ class DesktopDisplayEventHandler(
            logV("shouldCreateOrWarmUpDesk skipping reason: invalid display")
            return false
        }
        if (!supportsDesks(displayId)) {
        if (!desktopState.isDesktopModeSupportedOnDisplay(displayId)) {
            logV(
                "shouldCreateOrWarmUpDesk skipping displayId=%d reason: desktop ineligible",
                displayId,
@@ -170,18 +199,32 @@ class DesktopDisplayEventHandler(
        return true
    }

    // TODO: b/362720497 - connected/projected display considerations.
    private fun isDisplayDesktopFirst(displayId: Int): Boolean =
        displayId != Display.DEFAULT_DISPLAY

    // TODO: b/362720497 - connected/projected display considerations.
    private fun supportsDesks(displayId: Int): Boolean =
        desktopState.isDesktopModeSupportedOnDisplay(displayId)
    private fun isUserDesktopEligible(userId: Int): Boolean =
        !(DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_HSUM.isTrue &&
            UserManager.isHeadlessSystemUserMode() &&
            UserHandle.USER_SYSTEM == userId)

    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 class OnDisplayAreaChangeListener(
        private val onDisplayAreaChanged: (displayId: Int) -> Unit
    ) : RootTaskDisplayAreaListener {

        override fun onDisplayAreaAppeared(displayAreaInfo: DisplayAreaInfo) {
            onDisplayAreaChanged(displayAreaInfo.displayId)
        }

        override fun onDisplayAreaInfoChanged(displayAreaInfo: DisplayAreaInfo) {
            onDisplayAreaChanged(displayAreaInfo.displayId)
        }
    }

    companion object {
        private const val TAG = "DesktopDisplayEventHandler"
    }
+103 −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

import android.view.Display.DEFAULT_DISPLAY
import android.view.SurfaceControl
import android.window.DisplayAreaInfo
import android.window.DisplayAreaOrganizer
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.wm.shell.RootTaskDisplayAreaOrganizer.RootTaskDisplayAreaListener
import com.android.wm.shell.sysui.ShellInit
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/**
 * Tests for [RootTaskDisplayAreaOrganizerTest].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:RootTaskDisplayAreaOrganizerTest
 */
@SmallTest
@RunWith(AndroidJUnit4::class)
class RootTaskDisplayAreaOrganizerTest : ShellTestCase() {

    private val executor = TestShellExecutor()
    private val shellInit = ShellInit(executor)

    private lateinit var organizer: RootTaskDisplayAreaOrganizer

    @Before
    fun setUp() {
        organizer = RootTaskDisplayAreaOrganizer(executor, context, shellInit)
    }

    @Test
    fun registerListener_callsBackWithExistingRootTDA() {
        organizer.onDisplayAreaAppeared(createDisplayAreaInfo(FIRST_DISPLAY), SurfaceControl())
        organizer.onDisplayAreaAppeared(createDisplayAreaInfo(SECOND_DISPLAY), SurfaceControl())

        val listener = FakeRootTaskDisplayAreaListener()
        organizer.registerListener(FIRST_DISPLAY, listener)

        assertThat(listener.displayAreas).containsExactly(FIRST_DISPLAY)
    }

    @Test
    fun registerListener_otherForSameDisplay_callsBothBackWithExistingRootTDAs() {
        organizer.onDisplayAreaAppeared(createDisplayAreaInfo(FIRST_DISPLAY), SurfaceControl())
        organizer.onDisplayAreaAppeared(createDisplayAreaInfo(SECOND_DISPLAY), SurfaceControl())

        val listener1 = FakeRootTaskDisplayAreaListener()
        val listener2 = FakeRootTaskDisplayAreaListener()
        organizer.registerListener(SECOND_DISPLAY, listener1)
        organizer.registerListener(SECOND_DISPLAY, listener2)

        assertThat(listener1.displayAreas).containsExactly(SECOND_DISPLAY)
        assertThat(listener2.displayAreas).containsExactly(SECOND_DISPLAY)
    }

    @Test
    fun unregisterListener() {
        val listener = FakeRootTaskDisplayAreaListener()

        organizer.unregisterListener(FIRST_DISPLAY, listener)
        organizer.onDisplayAreaAppeared(createDisplayAreaInfo(FIRST_DISPLAY), SurfaceControl())

        assertThat(listener.displayAreas).doesNotContain(FIRST_DISPLAY)
    }

    private fun createDisplayAreaInfo(displayId: Int) = DisplayAreaInfo(
        MockToken().token(), displayId, DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER
    )

    private class FakeRootTaskDisplayAreaListener : RootTaskDisplayAreaListener {
        val displayAreas = mutableListOf<Int>()

        override fun onDisplayAreaAppeared(displayAreaInfo: DisplayAreaInfo) {
            displayAreas.add(displayAreaInfo.displayId)
        }
    }

    companion object {
        private const val FIRST_DISPLAY = DEFAULT_DISPLAY
        private const val SECOND_DISPLAY = 2
        private const val THIRD_DISPLAY = 3
    }
}
 No newline at end of file
+105 −48

File changed.

Preview size limit exceeded, changes collapsed.