1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.wm.shell.desktopmode 18 19 import android.content.Context 20 import android.view.Display 21 import android.view.Display.DEFAULT_DISPLAY 22 import android.window.DesktopExperienceFlags 23 import com.android.internal.protolog.ProtoLog 24 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 25 import com.android.wm.shell.common.DisplayController 26 import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener 27 import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener 28 import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer 29 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 30 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus 31 import com.android.wm.shell.sysui.ShellController 32 import com.android.wm.shell.sysui.ShellInit 33 import com.android.wm.shell.sysui.UserChangeListener 34 import kotlinx.coroutines.CoroutineScope 35 import kotlinx.coroutines.cancel 36 import kotlinx.coroutines.launch 37 38 /** Handles display events in desktop mode */ 39 class DesktopDisplayEventHandler( 40 private val context: Context, 41 shellInit: ShellInit, 42 private val mainScope: CoroutineScope, 43 private val shellController: ShellController, 44 private val displayController: DisplayController, 45 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, 46 private val desktopRepositoryInitializer: DesktopRepositoryInitializer, 47 private val desktopUserRepositories: DesktopUserRepositories, 48 private val desktopTasksController: DesktopTasksController, 49 private val desktopDisplayModeController: DesktopDisplayModeController, 50 ) : OnDisplaysChangedListener, OnDeskRemovedListener { 51 52 init { 53 shellInit.addInitCallback({ onInit() }, this) 54 } 55 56 private fun onInit() { 57 displayController.addDisplayWindowListener(this) 58 59 if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { 60 desktopTasksController.onDeskRemovedListener = this 61 62 shellController.addUserChangeListener( 63 object : UserChangeListener { 64 override fun onUserChanged(newUserId: Int, userContext: Context) { 65 val displayIds = rootTaskDisplayAreaOrganizer.displayIds 66 createDefaultDesksIfNeeded(displayIds.toSet(), newUserId) 67 } 68 } 69 ) 70 } 71 } 72 73 override fun onDisplayAdded(displayId: Int) { 74 if (displayId != DEFAULT_DISPLAY) { 75 desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId) 76 // The default display's windowing mode depends on the availability of the external 77 // display. So updating the default display's windowing mode here. 78 desktopDisplayModeController.updateDefaultDisplayWindowingMode() 79 } 80 81 createDefaultDesksIfNeeded(displayIds = setOf(displayId), userId = null) 82 } 83 84 override fun onDisplayRemoved(displayId: Int) { 85 if (displayId != DEFAULT_DISPLAY) { 86 desktopDisplayModeController.updateDefaultDisplayWindowingMode() 87 } 88 89 // TODO: b/362720497 - move desks in closing display to the remaining desk. 90 } 91 92 override fun onDesktopModeEligibleChanged(displayId: Int) { 93 if ( 94 DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue && 95 displayId != DEFAULT_DISPLAY 96 ) { 97 desktopDisplayModeController.updateExternalDisplayWindowingMode(displayId) 98 // The default display's windowing mode depends on the desktop eligibility of the 99 // external display. So updating the default display's windowing mode here. 100 desktopDisplayModeController.updateDefaultDisplayWindowingMode() 101 } 102 } 103 104 override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { 105 createDefaultDesksIfNeeded(setOf(lastDisplayId), userId = null) 106 } 107 108 private fun createDefaultDesksIfNeeded(displayIds: Set<Int>, userId: Int?) { 109 if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return 110 logV("createDefaultDesksIfNeeded displays=%s", displayIds) 111 mainScope.launch { 112 desktopRepositoryInitializer.isInitialized.collect { initialized -> 113 if (!initialized) return@collect 114 val repository = 115 userId?.let { desktopUserRepositories.getProfile(userId) } 116 ?: desktopUserRepositories.current 117 displayIds 118 .filter { displayId -> displayId != Display.INVALID_DISPLAY } 119 .filter { displayId -> supportsDesks(displayId) } 120 .filter { displayId -> repository.getNumberOfDesks(displayId) == 0 } 121 .also { displaysNeedingDesk -> 122 logV( 123 "createDefaultDesksIfNeeded creating default desks in displays=%s", 124 displaysNeedingDesk, 125 ) 126 } 127 .forEach { displayId -> 128 // TODO: b/393978539 - consider activating the desk on creation when 129 // applicable, such as for connected displays. 130 desktopTasksController.createDesk(displayId, repository.userId) 131 } 132 cancel() 133 } 134 } 135 } 136 137 // TODO: b/362720497 - connected/projected display considerations. 138 private fun supportsDesks(displayId: Int): Boolean = 139 DesktopModeStatus.canEnterDesktopMode(context) 140 141 private fun logV(msg: String, vararg arguments: Any?) { 142 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 143 } 144 145 companion object { 146 private const val TAG = "DesktopDisplayEventHandler" 147 } 148 } 149