1 /* <lambda>null2 * Copyright (C) 2025 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.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD 20 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 21 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN 22 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED 23 import android.app.WindowConfiguration.windowingModeToString 24 import android.content.Context 25 import android.hardware.input.InputManager 26 import android.os.Handler 27 import android.provider.Settings 28 import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS 29 import android.view.Display.DEFAULT_DISPLAY 30 import android.view.IWindowManager 31 import android.view.InputDevice 32 import android.view.WindowManager.TRANSIT_CHANGE 33 import android.window.DesktopExperienceFlags 34 import android.window.WindowContainerTransaction 35 import com.android.internal.annotations.VisibleForTesting 36 import com.android.internal.protolog.ProtoLog 37 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 38 import com.android.wm.shell.ShellTaskOrganizer 39 import com.android.wm.shell.common.DisplayController 40 import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider 41 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 42 import com.android.wm.shell.shared.annotations.ShellMainThread 43 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus 44 import com.android.wm.shell.transition.Transitions 45 46 /** Controls the display windowing mode in desktop mode */ 47 class DesktopDisplayModeController( 48 private val context: Context, 49 private val transitions: Transitions, 50 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, 51 private val windowManager: IWindowManager, 52 private val shellTaskOrganizer: ShellTaskOrganizer, 53 private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, 54 private val inputManager: InputManager, 55 private val displayController: DisplayController, 56 @ShellMainThread private val mainHandler: Handler, 57 ) { 58 59 private val inputDeviceListener = 60 object : InputManager.InputDeviceListener { 61 override fun onInputDeviceAdded(deviceId: Int) { 62 updateDefaultDisplayWindowingMode() 63 } 64 65 override fun onInputDeviceChanged(deviceId: Int) { 66 updateDefaultDisplayWindowingMode() 67 } 68 69 override fun onInputDeviceRemoved(deviceId: Int) { 70 updateDefaultDisplayWindowingMode() 71 } 72 } 73 74 init { 75 if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { 76 inputManager.registerInputDeviceListener(inputDeviceListener, mainHandler) 77 } 78 } 79 80 fun updateExternalDisplayWindowingMode(displayId: Int) { 81 if (!DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue) return 82 83 val desktopModeSupported = 84 displayController.getDisplay(displayId)?.let { display -> 85 DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) 86 } ?: false 87 if (!desktopModeSupported) return 88 89 // An external display should always be a freeform display when desktop mode is enabled. 90 updateDisplayWindowingMode(displayId, WINDOWING_MODE_FREEFORM) 91 } 92 93 fun updateDefaultDisplayWindowingMode() { 94 if (!DesktopExperienceFlags.ENABLE_DISPLAY_WINDOWING_MODE_SWITCHING.isTrue) return 95 96 updateDisplayWindowingMode(DEFAULT_DISPLAY, getTargetWindowingModeForDefaultDisplay()) 97 } 98 99 private fun updateDisplayWindowingMode(displayId: Int, targetDisplayWindowingMode: Int) { 100 val tdaInfo = 101 requireNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)) { 102 "DisplayAreaInfo of display#$displayId must be non-null." 103 } 104 val currentDisplayWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode 105 if (currentDisplayWindowingMode == targetDisplayWindowingMode) { 106 // Already in the target mode. 107 return 108 } 109 110 logV( 111 "Changing display#%d's windowing mode from %s to %s", 112 displayId, 113 windowingModeToString(currentDisplayWindowingMode), 114 windowingModeToString(targetDisplayWindowingMode), 115 ) 116 117 val wct = WindowContainerTransaction() 118 wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) 119 shellTaskOrganizer 120 .getRunningTasks(displayId) 121 .filter { it.activityType == ACTIVITY_TYPE_STANDARD } 122 .forEach { 123 // TODO: b/391965153 - Reconsider the logic under multi-desk window hierarchy 124 when (it.windowingMode) { 125 currentDisplayWindowingMode -> { 126 wct.setWindowingMode(it.token, currentDisplayWindowingMode) 127 } 128 targetDisplayWindowingMode -> { 129 wct.setWindowingMode(it.token, WINDOWING_MODE_UNDEFINED) 130 } 131 } 132 } 133 // The override windowing mode of DesktopWallpaper can be UNDEFINED on fullscreen-display 134 // right after the first launch while its resolved windowing mode is FULLSCREEN. We here 135 // it has the FULLSCREEN override windowing mode. 136 desktopWallpaperActivityTokenProvider.getToken(displayId)?.let { token -> 137 wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) 138 } 139 transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) 140 } 141 142 // Do not directly use this method to check the state of desktop-first mode. Check the display 143 // windowing mode instead. 144 private fun canDesktopFirstModeBeEnabledOnDefaultDisplay(): Boolean { 145 if (isDefaultDisplayDesktopEligible()) { 146 if (isExtendedDisplayEnabled() && hasExternalDisplay()) { 147 return true 148 } 149 if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { 150 if (hasAnyTouchpadDevice() && hasAnyPhysicalKeyboardDevice()) { 151 return true 152 } 153 } 154 } 155 return false 156 } 157 158 @VisibleForTesting 159 fun getTargetWindowingModeForDefaultDisplay(): Int { 160 if (canDesktopFirstModeBeEnabledOnDefaultDisplay()) { 161 return WINDOWING_MODE_FREEFORM 162 } 163 164 return if (DesktopExperienceFlags.FORM_FACTOR_BASED_DESKTOP_FIRST_SWITCH.isTrue) { 165 WINDOWING_MODE_FULLSCREEN 166 } else { 167 // If form factor-based desktop first switch is disabled, use the default display 168 // windowing mode here to keep the freeform mode for some form factors (e.g., 169 // FEATURE_PC). 170 windowManager.getWindowingMode(DEFAULT_DISPLAY) 171 } 172 } 173 174 private fun isExtendedDisplayEnabled(): Boolean { 175 if (DesktopExperienceFlags.ENABLE_DISPLAY_CONTENT_MODE_MANAGEMENT.isTrue) { 176 return rootTaskDisplayAreaOrganizer 177 .getDisplayIds() 178 .filter { it != DEFAULT_DISPLAY } 179 .any { displayId -> 180 displayController.getDisplay(displayId)?.let { display -> 181 DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) 182 } ?: false 183 } 184 } 185 186 return 0 != 187 Settings.Global.getInt( 188 context.contentResolver, 189 DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS, 190 0, 191 ) 192 } 193 194 private fun hasExternalDisplay() = 195 rootTaskDisplayAreaOrganizer.getDisplayIds().any { it != DEFAULT_DISPLAY } 196 197 private fun hasAnyTouchpadDevice() = 198 inputManager.inputDeviceIds.any { deviceId -> 199 inputManager.getInputDevice(deviceId)?.let { device -> 200 device.supportsSource(InputDevice.SOURCE_TOUCHPAD) && device.isEnabled() 201 } ?: false 202 } 203 204 private fun hasAnyPhysicalKeyboardDevice() = 205 inputManager.inputDeviceIds.any { deviceId -> 206 inputManager.getInputDevice(deviceId)?.let { device -> 207 !device.isVirtual() && device.isFullKeyboard() && device.isEnabled() 208 } ?: false 209 } 210 211 private fun isDefaultDisplayDesktopEligible(): Boolean { 212 val display = 213 requireNotNull(displayController.getDisplay(DEFAULT_DISPLAY)) { 214 "Display object of DEFAULT_DISPLAY must be non-null." 215 } 216 return DesktopModeStatus.isDesktopModeSupportedOnDisplay(context, display) 217 } 218 219 private fun logV(msg: String, vararg arguments: Any?) { 220 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) 221 } 222 223 companion object { 224 private const val TAG = "DesktopDisplayModeController" 225 } 226 } 227