1 /* <lambda>null2 * Copyright (C) 2022 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.ActivityManager 20 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME 21 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD 22 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 23 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN 24 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED 25 import android.app.WindowConfiguration.WindowingMode 26 import android.content.Context 27 import android.os.IBinder 28 import android.os.SystemProperties 29 import android.view.SurfaceControl 30 import android.view.WindowManager.TRANSIT_CHANGE 31 import android.view.WindowManager.TRANSIT_NONE 32 import android.view.WindowManager.TRANSIT_OPEN 33 import android.view.WindowManager.TRANSIT_TO_FRONT 34 import android.window.TransitionInfo 35 import android.window.TransitionRequestInfo 36 import android.window.WindowContainerToken 37 import android.window.WindowContainerTransaction 38 import androidx.annotation.BinderThread 39 import com.android.internal.protolog.common.ProtoLog 40 import com.android.wm.shell.ShellTaskOrganizer 41 import com.android.wm.shell.common.ExecutorUtils 42 import com.android.wm.shell.common.ExternalInterfaceBinder 43 import com.android.wm.shell.common.RemoteCallable 44 import com.android.wm.shell.common.ShellExecutor 45 import com.android.wm.shell.common.annotations.ExternalThread 46 import com.android.wm.shell.common.annotations.ShellMainThread 47 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener 48 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 49 import com.android.wm.shell.sysui.ShellController 50 import com.android.wm.shell.sysui.ShellInit 51 import com.android.wm.shell.sysui.ShellSharedConstants 52 import com.android.wm.shell.transition.Transitions 53 import java.util.concurrent.Executor 54 import java.util.function.Consumer 55 56 /** Handles moving tasks in and out of desktop */ 57 class DesktopTasksController( 58 private val context: Context, 59 shellInit: ShellInit, 60 private val shellController: ShellController, 61 private val shellTaskOrganizer: ShellTaskOrganizer, 62 private val transitions: Transitions, 63 private val desktopModeTaskRepository: DesktopModeTaskRepository, 64 @ShellMainThread private val mainExecutor: ShellExecutor 65 ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler { 66 67 private val desktopMode: DesktopModeImpl 68 69 init { 70 desktopMode = DesktopModeImpl() 71 if (DesktopModeStatus.isProto2Enabled()) { 72 shellInit.addInitCallback({ onInit() }, this) 73 } 74 } 75 76 private fun onInit() { 77 ProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") 78 shellController.addExternalInterface( 79 ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, 80 { createExternalInterface() }, 81 this 82 ) 83 transitions.addHandler(this) 84 } 85 86 /** Show all tasks, that are part of the desktop, on top of launcher */ 87 fun showDesktopApps() { 88 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "showDesktopApps") 89 val wct = WindowContainerTransaction() 90 bringDesktopAppsToFront(wct) 91 92 // Execute transaction if there are pending operations 93 if (!wct.isEmpty) { 94 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 95 // TODO(b/268662477): add animation for the transition 96 transitions.startTransition(TRANSIT_NONE, wct, null /* handler */) 97 } else { 98 shellTaskOrganizer.applyTransaction(wct) 99 } 100 } 101 } 102 103 /** Get number of tasks that are marked as visible */ 104 fun getVisibleTaskCount(): Int { 105 return desktopModeTaskRepository.getVisibleTaskCount() 106 } 107 108 /** Move a task with given `taskId` to desktop */ 109 fun moveToDesktop(taskId: Int) { 110 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToDesktop(task) } 111 } 112 113 /** Move a task to desktop */ 114 fun moveToDesktop(task: ActivityManager.RunningTaskInfo) { 115 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDesktop: %d", task.taskId) 116 117 val wct = WindowContainerTransaction() 118 // Bring other apps to front first 119 bringDesktopAppsToFront(wct) 120 addMoveToDesktopChanges(wct, task.token) 121 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 122 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 123 } else { 124 shellTaskOrganizer.applyTransaction(wct) 125 } 126 } 127 128 /** Move a task with given `taskId` to fullscreen */ 129 fun moveToFullscreen(taskId: Int) { 130 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) } 131 } 132 133 /** Move a task to fullscreen */ 134 fun moveToFullscreen(task: ActivityManager.RunningTaskInfo) { 135 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToFullscreen: %d", task.taskId) 136 137 val wct = WindowContainerTransaction() 138 addMoveToFullscreenChanges(wct, task.token) 139 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 140 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 141 } else { 142 shellTaskOrganizer.applyTransaction(wct) 143 } 144 } 145 146 /** 147 * Get windowing move for a given `taskId` 148 * 149 * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found 150 */ 151 @WindowingMode 152 fun getTaskWindowingMode(taskId: Int): Int { 153 return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode 154 ?: WINDOWING_MODE_UNDEFINED 155 } 156 157 private fun bringDesktopAppsToFront(wct: WindowContainerTransaction) { 158 ProtoLog.v(WM_SHELL_DESKTOP_MODE, "bringDesktopAppsToFront") 159 val activeTasks = desktopModeTaskRepository.getActiveTasks() 160 161 // First move home to front and then other tasks on top of it 162 moveHomeTaskToFront(wct) 163 164 val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() 165 activeTasks 166 // Sort descending as the top task is at index 0. It should be ordered to top last 167 .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) } 168 .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } 169 .forEach { task -> wct.reorder(task.token, true /* onTop */) } 170 } 171 172 private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { 173 shellTaskOrganizer 174 .getRunningTasks(context.displayId) 175 .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } 176 ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } 177 } 178 179 override fun getContext(): Context { 180 return context 181 } 182 183 override fun getRemoteCallExecutor(): ShellExecutor { 184 return mainExecutor 185 } 186 187 override fun startAnimation( 188 transition: IBinder, 189 info: TransitionInfo, 190 startTransaction: SurfaceControl.Transaction, 191 finishTransaction: SurfaceControl.Transaction, 192 finishCallback: Transitions.TransitionFinishCallback 193 ): Boolean { 194 // This handler should never be the sole handler, so should not animate anything. 195 return false 196 } 197 198 override fun handleRequest( 199 transition: IBinder, 200 request: TransitionRequestInfo 201 ): WindowContainerTransaction? { 202 // Check if we should skip handling this transition 203 val task: ActivityManager.RunningTaskInfo? = request.triggerTask 204 val shouldHandleRequest = 205 when { 206 // Only handle open or to front transitions 207 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> false 208 // Only handle when it is a task transition 209 task == null -> false 210 // Only handle standard type tasks 211 task.activityType != ACTIVITY_TYPE_STANDARD -> false 212 // Only handle fullscreen or freeform tasks 213 task.windowingMode != WINDOWING_MODE_FULLSCREEN && 214 task.windowingMode != WINDOWING_MODE_FREEFORM -> false 215 // Otherwise process it 216 else -> true 217 } 218 219 if (!shouldHandleRequest) { 220 return null 221 } 222 223 val activeTasks = desktopModeTaskRepository.getActiveTasks() 224 225 // Check if we should switch a fullscreen task to freeform 226 if (task?.windowingMode == WINDOWING_MODE_FULLSCREEN) { 227 // If there are any visible desktop tasks, switch the task to freeform 228 if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { 229 ProtoLog.d( 230 WM_SHELL_DESKTOP_MODE, 231 "DesktopTasksController#handleRequest: switch fullscreen task to freeform," + 232 " taskId=%d", 233 task.taskId 234 ) 235 return WindowContainerTransaction().also { wct -> 236 addMoveToDesktopChanges(wct, task.token) 237 } 238 } 239 } 240 241 // CHeck if we should switch a freeform task to fullscreen 242 if (task?.windowingMode == WINDOWING_MODE_FREEFORM) { 243 // If no visible desktop tasks, switch this task to freeform as the transition came 244 // outside of this controller 245 if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { 246 ProtoLog.d( 247 WM_SHELL_DESKTOP_MODE, 248 "DesktopTasksController#handleRequest: switch freeform task to fullscreen," + 249 " taskId=%d", 250 task.taskId 251 ) 252 return WindowContainerTransaction().also { wct -> 253 addMoveToFullscreenChanges(wct, task.token) 254 } 255 } 256 } 257 return null 258 } 259 260 private fun addMoveToDesktopChanges( 261 wct: WindowContainerTransaction, 262 token: WindowContainerToken 263 ) { 264 wct.setWindowingMode(token, WINDOWING_MODE_FREEFORM) 265 wct.reorder(token, true /* onTop */) 266 if (isDesktopDensityOverrideSet()) { 267 wct.setDensityDpi(token, getDesktopDensityDpi()) 268 } 269 } 270 271 private fun addMoveToFullscreenChanges( 272 wct: WindowContainerTransaction, 273 token: WindowContainerToken 274 ) { 275 wct.setWindowingMode(token, WINDOWING_MODE_FULLSCREEN) 276 wct.setBounds(token, null) 277 if (isDesktopDensityOverrideSet()) { 278 wct.setDensityDpi(token, getFullscreenDensityDpi()) 279 } 280 } 281 282 private fun getFullscreenDensityDpi(): Int { 283 return context.resources.displayMetrics.densityDpi 284 } 285 286 private fun getDesktopDensityDpi(): Int { 287 return DESKTOP_DENSITY_OVERRIDE 288 } 289 290 /** Creates a new instance of the external interface to pass to another process. */ 291 private fun createExternalInterface(): ExternalInterfaceBinder { 292 return IDesktopModeImpl(this) 293 } 294 295 /** Get connection interface between sysui and shell */ 296 fun asDesktopMode(): DesktopMode { 297 return desktopMode 298 } 299 300 /** 301 * Adds a listener to find out about changes in the visibility of freeform tasks. 302 * 303 * @param listener the listener to add. 304 * @param callbackExecutor the executor to call the listener on. 305 */ 306 fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) { 307 desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor) 308 } 309 310 /** The interface for calls from outside the shell, within the host process. */ 311 @ExternalThread 312 private inner class DesktopModeImpl : DesktopMode { 313 override fun addListener(listener: VisibleTasksListener, callbackExecutor: Executor) { 314 mainExecutor.execute { 315 this@DesktopTasksController.addListener(listener, callbackExecutor) 316 } 317 } 318 } 319 320 /** The interface for calls from outside the host process. */ 321 @BinderThread 322 private class IDesktopModeImpl(private var controller: DesktopTasksController?) : 323 IDesktopMode.Stub(), ExternalInterfaceBinder { 324 /** Invalidates this instance, preventing future calls from updating the controller. */ 325 override fun invalidate() { 326 controller = null 327 } 328 329 override fun showDesktopApps() { 330 ExecutorUtils.executeRemoteCallWithTaskPermission( 331 controller, 332 "showDesktopApps", 333 Consumer(DesktopTasksController::showDesktopApps) 334 ) 335 } 336 337 override fun getVisibleTaskCount(): Int { 338 val result = IntArray(1) 339 ExecutorUtils.executeRemoteCallWithTaskPermission( 340 controller, 341 "getVisibleTaskCount", 342 { controller -> result[0] = controller.getVisibleTaskCount() }, 343 true /* blocking */ 344 ) 345 return result[0] 346 } 347 } 348 349 companion object { 350 private val DESKTOP_DENSITY_OVERRIDE = 351 SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 0) 352 private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000) 353 354 /** 355 * Check if desktop density override is enabled 356 */ 357 @JvmStatic 358 fun isDesktopDensityOverrideSet(): Boolean { 359 return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE 360 } 361 } 362 } 363