• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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