1 /* 2 * 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.R 20 import android.app.ActivityManager.RunningTaskInfo 21 import android.app.WindowConfiguration 22 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME 23 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD 24 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM 25 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN 26 import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW 27 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED 28 import android.app.WindowConfiguration.WindowingMode 29 import android.content.Context 30 import android.content.res.TypedArray 31 import android.graphics.Point 32 import android.graphics.PointF 33 import android.graphics.Rect 34 import android.graphics.Region 35 import android.os.IBinder 36 import android.os.SystemProperties 37 import android.util.DisplayMetrics.DENSITY_DEFAULT 38 import android.view.SurfaceControl 39 import android.view.WindowManager.TRANSIT_CHANGE 40 import android.view.WindowManager.TRANSIT_NONE 41 import android.view.WindowManager.TRANSIT_OPEN 42 import android.view.WindowManager.TRANSIT_TO_FRONT 43 import android.window.TransitionInfo 44 import android.window.TransitionRequestInfo 45 import android.window.WindowContainerTransaction 46 import androidx.annotation.BinderThread 47 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 48 import com.android.wm.shell.ShellTaskOrganizer 49 import com.android.wm.shell.common.DisplayController 50 import com.android.wm.shell.common.ExecutorUtils 51 import com.android.wm.shell.common.ExternalInterfaceBinder 52 import com.android.wm.shell.common.LaunchAdjacentController 53 import com.android.wm.shell.common.RemoteCallable 54 import com.android.wm.shell.common.ShellExecutor 55 import com.android.wm.shell.common.SingleInstanceRemoteListener 56 import com.android.wm.shell.common.SyncTransactionQueue 57 import com.android.wm.shell.common.annotations.ExternalThread 58 import com.android.wm.shell.common.annotations.ShellMainThread 59 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT 60 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT 61 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository.VisibleTasksListener 62 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.TO_DESKTOP_INDICATOR 63 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE 64 import com.android.wm.shell.splitscreen.SplitScreenController 65 import com.android.wm.shell.sysui.ShellCommandHandler 66 import com.android.wm.shell.sysui.ShellController 67 import com.android.wm.shell.sysui.ShellInit 68 import com.android.wm.shell.sysui.ShellSharedConstants 69 import com.android.wm.shell.transition.Transitions 70 import com.android.wm.shell.util.KtProtoLog 71 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration 72 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator 73 import java.io.PrintWriter 74 import java.util.concurrent.Executor 75 import java.util.function.Consumer 76 77 /** Handles moving tasks in and out of desktop */ 78 class DesktopTasksController( 79 private val context: Context, 80 shellInit: ShellInit, 81 private val shellCommandHandler: ShellCommandHandler, 82 private val shellController: ShellController, 83 private val displayController: DisplayController, 84 private val shellTaskOrganizer: ShellTaskOrganizer, 85 private val syncQueue: SyncTransactionQueue, 86 private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, 87 private val transitions: Transitions, 88 private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, 89 private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, 90 private val toggleResizeDesktopTaskTransitionHandler: 91 ToggleResizeDesktopTaskTransitionHandler, 92 private val desktopModeTaskRepository: DesktopModeTaskRepository, 93 private val launchAdjacentController: LaunchAdjacentController, 94 @ShellMainThread private val mainExecutor: ShellExecutor 95 ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler { 96 97 private val desktopMode: DesktopModeImpl 98 private var visualIndicator: DesktopModeVisualIndicator? = null 99 private val mOnAnimationFinishedCallback = Consumer<SurfaceControl.Transaction> { 100 t: SurfaceControl.Transaction -> 101 visualIndicator?.releaseVisualIndicator(t) 102 visualIndicator = null 103 } 104 private val taskVisibilityListener = object : VisibleTasksListener { 105 override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) { 106 launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks 107 } 108 } 109 110 private val transitionAreaHeight 111 get() = context.resources.getDimensionPixelSize( 112 com.android.wm.shell.R.dimen.desktop_mode_transition_area_height) 113 114 private val transitionAreaWidth 115 get() = context.resources.getDimensionPixelSize( 116 com.android.wm.shell.R.dimen.desktop_mode_transition_area_width) 117 118 // This is public to avoid cyclic dependency; it is set by SplitScreenController 119 lateinit var splitScreenController: SplitScreenController 120 121 init { 122 desktopMode = DesktopModeImpl() 123 if (DesktopModeStatus.isProto2Enabled()) { 124 shellInit.addInitCallback({ onInit() }, this) 125 } 126 } 127 128 private fun onInit() { 129 KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "Initialize DesktopTasksController") 130 shellCommandHandler.addDumpCallback(this::dump, this) 131 shellController.addExternalInterface( 132 ShellSharedConstants.KEY_EXTRA_SHELL_DESKTOP_MODE, 133 { createExternalInterface() }, 134 this 135 ) 136 transitions.addHandler(this) 137 desktopModeTaskRepository.addVisibleTasksListener(taskVisibilityListener, mainExecutor) 138 } 139 140 /** Show all tasks, that are part of the desktop, on top of launcher */ 141 fun showDesktopApps(displayId: Int) { 142 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: showDesktopApps") 143 val wct = WindowContainerTransaction() 144 // TODO(b/278084491): pass in display id 145 bringDesktopAppsToFront(displayId, wct) 146 147 // Execute transaction if there are pending operations 148 if (!wct.isEmpty) { 149 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 150 // TODO(b/268662477): add animation for the transition 151 transitions.startTransition(TRANSIT_NONE, wct, null /* handler */) 152 } else { 153 shellTaskOrganizer.applyTransaction(wct) 154 } 155 } 156 } 157 158 /** 159 * Stash desktop tasks on display with id [displayId]. 160 * 161 * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps 162 * launched in this state will be added to the desktop. Existing desktop tasks will be brought 163 * back to front during the launch. 164 */ 165 fun stashDesktopApps(displayId: Int) { 166 if (DesktopModeStatus.isStashingEnabled()) { 167 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps") 168 desktopModeTaskRepository.setStashed(displayId, true) 169 } 170 } 171 172 /** 173 * Clear the stashed state for the given display 174 */ 175 fun hideStashedDesktopApps(displayId: Int) { 176 if (DesktopModeStatus.isStashingEnabled()) { 177 KtProtoLog.v( 178 WM_SHELL_DESKTOP_MODE, 179 "DesktopTasksController: hideStashedApps displayId=%d", 180 displayId 181 ) 182 desktopModeTaskRepository.setStashed(displayId, false) 183 } 184 } 185 186 /** Get number of tasks that are marked as visible */ 187 fun getVisibleTaskCount(displayId: Int): Int { 188 return desktopModeTaskRepository.getVisibleTaskCount(displayId) 189 } 190 191 /** Move a task with given `taskId` to desktop */ 192 fun moveToDesktop( 193 decor: DesktopModeWindowDecoration, 194 taskId: Int, 195 wct: WindowContainerTransaction = WindowContainerTransaction() 196 ) { 197 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { 198 task -> moveToDesktop(decor, task, wct) 199 } 200 } 201 202 /** 203 * Move a task to desktop 204 */ 205 fun moveToDesktop( 206 decor: DesktopModeWindowDecoration, 207 task: RunningTaskInfo, 208 wct: WindowContainerTransaction = WindowContainerTransaction() 209 ) { 210 KtProtoLog.v( 211 WM_SHELL_DESKTOP_MODE, 212 "DesktopTasksController: moveToDesktop taskId=%d", 213 task.taskId 214 ) 215 // Bring other apps to front first 216 bringDesktopAppsToFront(task.displayId, wct) 217 addMoveToDesktopChanges(wct, task) 218 219 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 220 enterDesktopTaskTransitionHandler.moveToDesktop(wct, decor) 221 } else { 222 shellTaskOrganizer.applyTransaction(wct) 223 } 224 } 225 226 /** 227 * The first part of the animated move to desktop transition. Applies the changes to move task 228 * to desktop mode and sets the taskBounds to the passed in bounds, startBounds. This is 229 * followed with a call to {@link finishMoveToDesktop} or {@link cancelMoveToDesktop}. 230 */ 231 fun startMoveToDesktop( 232 taskInfo: RunningTaskInfo, 233 startBounds: Rect, 234 dragToDesktopValueAnimator: MoveToDesktopAnimator 235 ) { 236 KtProtoLog.v( 237 WM_SHELL_DESKTOP_MODE, 238 "DesktopTasksController: startMoveToDesktop taskId=%d", 239 taskInfo.taskId 240 ) 241 val wct = WindowContainerTransaction() 242 moveHomeTaskToFront(wct) 243 addMoveToDesktopChanges(wct, taskInfo) 244 wct.setBounds(taskInfo.token, startBounds) 245 246 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 247 enterDesktopTaskTransitionHandler.startMoveToDesktop(wct, dragToDesktopValueAnimator, 248 mOnAnimationFinishedCallback) 249 } else { 250 shellTaskOrganizer.applyTransaction(wct) 251 } 252 } 253 254 /** 255 * The second part of the animated move to desktop transition, called after 256 * {@link startMoveToDesktop}. Brings apps to front and sets freeform task bounds. 257 */ 258 private fun finalizeMoveToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) { 259 KtProtoLog.v( 260 WM_SHELL_DESKTOP_MODE, 261 "DesktopTasksController: finalizeMoveToDesktop taskId=%d", 262 taskInfo.taskId 263 ) 264 val wct = WindowContainerTransaction() 265 bringDesktopAppsToFront(taskInfo.displayId, wct) 266 addMoveToDesktopChanges(wct, taskInfo) 267 wct.setBounds(taskInfo.token, freeformBounds) 268 269 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 270 enterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct, 271 mOnAnimationFinishedCallback) 272 } else { 273 shellTaskOrganizer.applyTransaction(wct) 274 releaseVisualIndicator() 275 } 276 } 277 278 /** 279 * Perform needed cleanup transaction once animation is complete. Bounds need to be set 280 * here instead of initial wct to both avoid flicker and to have task bounds to use for 281 * the staging animation. 282 * 283 * @param taskInfo task entering split that requires a bounds update 284 */ 285 fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { 286 val wct = WindowContainerTransaction() 287 wct.setBounds(taskInfo.token, Rect()) 288 shellTaskOrganizer.applyTransaction(wct) 289 } 290 291 /** Move a task with given `taskId` to fullscreen */ 292 fun moveToFullscreen(taskId: Int) { 293 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) } 294 } 295 296 /** Move a task to fullscreen */ 297 fun moveToFullscreen(task: RunningTaskInfo) { 298 KtProtoLog.v( 299 WM_SHELL_DESKTOP_MODE, 300 "DesktopTasksController: moveToFullscreen taskId=%d", 301 task.taskId 302 ) 303 304 val wct = WindowContainerTransaction() 305 addMoveToFullscreenChanges(wct, task) 306 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 307 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 308 } else { 309 shellTaskOrganizer.applyTransaction(wct) 310 } 311 } 312 313 /** Move a desktop app to split screen. */ 314 fun moveToSplit(task: RunningTaskInfo) { 315 KtProtoLog.v( 316 WM_SHELL_DESKTOP_MODE, 317 "DesktopTasksController: moveToSplit taskId=%d", 318 task.taskId 319 ) 320 val wct = WindowContainerTransaction() 321 wct.setWindowingMode(task.token, WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW) 322 wct.setBounds(task.token, Rect()) 323 wct.setDensityDpi(task.token, getDefaultDensityDpi()) 324 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 325 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 326 } else { 327 shellTaskOrganizer.applyTransaction(wct) 328 } 329 } 330 331 /** 332 * The second part of the animated move to desktop transition, called after 333 * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen 334 * and released back into status bar area. 335 */ 336 fun cancelMoveToDesktop(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) { 337 KtProtoLog.v( 338 WM_SHELL_DESKTOP_MODE, 339 "DesktopTasksController: cancelMoveToDesktop taskId=%d", 340 task.taskId 341 ) 342 val wct = WindowContainerTransaction() 343 wct.setBounds(task.token, Rect()) 344 345 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 346 enterDesktopTaskTransitionHandler.startCancelMoveToDesktopMode(wct, 347 moveToDesktopAnimator) { t -> 348 val callbackWCT = WindowContainerTransaction() 349 visualIndicator?.releaseVisualIndicator(t) 350 visualIndicator = null 351 addMoveToFullscreenChanges(callbackWCT, task) 352 transitions.startTransition(TRANSIT_CHANGE, callbackWCT, null /* handler */) 353 } 354 } else { 355 addMoveToFullscreenChanges(wct, task) 356 shellTaskOrganizer.applyTransaction(wct) 357 releaseVisualIndicator() 358 } 359 } 360 361 private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) { 362 KtProtoLog.v( 363 WM_SHELL_DESKTOP_MODE, 364 "DesktopTasksController: moveToFullscreen with animation taskId=%d", 365 task.taskId 366 ) 367 val wct = WindowContainerTransaction() 368 addMoveToFullscreenChanges(wct, task) 369 370 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 371 exitDesktopTaskTransitionHandler.startTransition( 372 Transitions.TRANSIT_EXIT_DESKTOP_MODE, wct, position, mOnAnimationFinishedCallback) 373 } else { 374 shellTaskOrganizer.applyTransaction(wct) 375 releaseVisualIndicator() 376 } 377 } 378 379 /** Move a task to the front */ 380 fun moveTaskToFront(taskId: Int) { 381 shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveTaskToFront(task) } 382 } 383 384 /** Move a task to the front */ 385 fun moveTaskToFront(taskInfo: RunningTaskInfo) { 386 KtProtoLog.v( 387 WM_SHELL_DESKTOP_MODE, 388 "DesktopTasksController: moveTaskToFront taskId=%d", 389 taskInfo.taskId 390 ) 391 392 val wct = WindowContainerTransaction() 393 wct.reorder(taskInfo.token, true) 394 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 395 transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) 396 } else { 397 shellTaskOrganizer.applyTransaction(wct) 398 } 399 } 400 401 /** 402 * Move task to the next display. 403 * 404 * Queries all current known display ids and sorts them in ascending order. Then iterates 405 * through the list and looks for the display id that is larger than the display id for 406 * the passed in task. If a display with a higher id is not found, iterates through the list and 407 * finds the first display id that is not the display id for the passed in task. 408 * 409 * If a display matching the above criteria is found, re-parents the task to that display. 410 * No-op if no such display is found. 411 */ 412 fun moveToNextDisplay(taskId: Int) { 413 val task = shellTaskOrganizer.getRunningTaskInfo(taskId) 414 if (task == null) { 415 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) 416 return 417 } 418 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d", 419 taskId, task.displayId) 420 421 val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() 422 // Get the first display id that is higher than current task display id 423 var newDisplayId = displayIds.firstOrNull { displayId -> displayId > task.displayId } 424 if (newDisplayId == null) { 425 // No display with a higher id, get the first display id that is not the task display id 426 newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId } 427 } 428 if (newDisplayId == null) { 429 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found") 430 return 431 } 432 moveToDisplay(task, newDisplayId) 433 } 434 435 /** 436 * Move [task] to display with [displayId]. 437 * 438 * No-op if task is already on that display per [RunningTaskInfo.displayId]. 439 */ 440 private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { 441 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d", 442 task.taskId, displayId) 443 444 if (task.displayId == displayId) { 445 KtProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") 446 return 447 } 448 449 val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) 450 if (displayAreaInfo == null) { 451 KtProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found") 452 return 453 } 454 455 val wct = WindowContainerTransaction() 456 wct.reparent(task.token, displayAreaInfo.token, true /* onTop */) 457 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 458 transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) 459 } else { 460 shellTaskOrganizer.applyTransaction(wct) 461 } 462 } 463 464 /** Quick-resizes a desktop task, toggling between the stable bounds and the default bounds. */ 465 fun toggleDesktopTaskSize(taskInfo: RunningTaskInfo, windowDecor: DesktopModeWindowDecoration) { 466 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 467 468 val stableBounds = Rect() 469 displayLayout.getStableBounds(stableBounds) 470 val destinationBounds = Rect() 471 if (taskInfo.configuration.windowConfiguration.bounds == stableBounds) { 472 // The desktop task is currently occupying the whole stable bounds, toggle to the 473 // default bounds. 474 getDefaultDesktopTaskBounds( 475 density = taskInfo.configuration.densityDpi.toFloat() / DENSITY_DEFAULT, 476 stableBounds = stableBounds, 477 outBounds = destinationBounds 478 ) 479 } else { 480 // Toggle to the stable bounds. 481 destinationBounds.set(stableBounds) 482 } 483 484 val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) 485 if (Transitions.ENABLE_SHELL_TRANSITIONS) { 486 toggleResizeDesktopTaskTransitionHandler.startTransition( 487 wct, 488 taskInfo.taskId, 489 windowDecor 490 ) 491 } else { 492 shellTaskOrganizer.applyTransaction(wct) 493 } 494 } 495 496 private fun getDefaultDesktopTaskBounds(density: Float, stableBounds: Rect, outBounds: Rect) { 497 val width = (DESKTOP_MODE_DEFAULT_WIDTH_DP * density + 0.5f).toInt() 498 val height = (DESKTOP_MODE_DEFAULT_HEIGHT_DP * density + 0.5f).toInt() 499 outBounds.set(0, 0, width, height) 500 // Center the task in stable bounds 501 outBounds.offset( 502 stableBounds.centerX() - outBounds.centerX(), 503 stableBounds.centerY() - outBounds.centerY() 504 ) 505 } 506 507 /** 508 * Get windowing move for a given `taskId` 509 * 510 * @return [WindowingMode] for the task or [WINDOWING_MODE_UNDEFINED] if task is not found 511 */ 512 @WindowingMode 513 fun getTaskWindowingMode(taskId: Int): Int { 514 return shellTaskOrganizer.getRunningTaskInfo(taskId)?.windowingMode 515 ?: WINDOWING_MODE_UNDEFINED 516 } 517 518 private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) { 519 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront") 520 val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId) 521 522 // First move home to front and then other tasks on top of it 523 moveHomeTaskToFront(wct) 524 525 val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() 526 activeTasks 527 // Sort descending as the top task is at index 0. It should be ordered to top last 528 .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) } 529 .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } 530 .forEach { task -> wct.reorder(task.token, true /* onTop */) } 531 } 532 533 private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { 534 shellTaskOrganizer 535 .getRunningTasks(context.displayId) 536 .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } 537 ?.let { homeTask -> wct.reorder(homeTask.getToken(), true /* onTop */) } 538 } 539 540 private fun releaseVisualIndicator() { 541 val t = SurfaceControl.Transaction() 542 visualIndicator?.releaseVisualIndicator(t) 543 visualIndicator = null 544 syncQueue.runInSync { transaction -> 545 transaction.merge(t) 546 t.close() 547 } 548 } 549 550 override fun getContext(): Context { 551 return context 552 } 553 554 override fun getRemoteCallExecutor(): ShellExecutor { 555 return mainExecutor 556 } 557 558 override fun startAnimation( 559 transition: IBinder, 560 info: TransitionInfo, 561 startTransaction: SurfaceControl.Transaction, 562 finishTransaction: SurfaceControl.Transaction, 563 finishCallback: Transitions.TransitionFinishCallback 564 ): Boolean { 565 // This handler should never be the sole handler, so should not animate anything. 566 return false 567 } 568 569 override fun handleRequest( 570 transition: IBinder, 571 request: TransitionRequestInfo 572 ): WindowContainerTransaction? { 573 KtProtoLog.v( 574 WM_SHELL_DESKTOP_MODE, 575 "DesktopTasksController: handleRequest request=%s", 576 request 577 ) 578 // Check if we should skip handling this transition 579 var reason = "" 580 val triggerTask = request.triggerTask 581 val shouldHandleRequest = 582 when { 583 // Only handle open or to front transitions 584 request.type != TRANSIT_OPEN && request.type != TRANSIT_TO_FRONT -> { 585 reason = "transition type not handled (${request.type})" 586 false 587 } 588 // Only handle when it is a task transition 589 triggerTask == null -> { 590 reason = "triggerTask is null" 591 false 592 } 593 // Only handle standard type tasks 594 triggerTask.activityType != ACTIVITY_TYPE_STANDARD -> { 595 reason = "activityType not handled (${triggerTask.activityType})" 596 false 597 } 598 // Only handle fullscreen or freeform tasks 599 triggerTask.windowingMode != WINDOWING_MODE_FULLSCREEN && 600 triggerTask.windowingMode != WINDOWING_MODE_FREEFORM -> { 601 reason = "windowingMode not handled (${triggerTask.windowingMode})" 602 false 603 } 604 // Otherwise process it 605 else -> true 606 } 607 608 if (!shouldHandleRequest) { 609 KtProtoLog.v( 610 WM_SHELL_DESKTOP_MODE, 611 "DesktopTasksController: skipping handleRequest reason=%s", 612 reason 613 ) 614 return null 615 } 616 617 val result = triggerTask?.let { task -> 618 when { 619 // If display has tasks stashed, handle as stashed launch 620 desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) 621 // Check if fullscreen task should be updated 622 task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) 623 // Check if freeform task should be updated 624 task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) 625 else -> { 626 null 627 } 628 } 629 } 630 KtProtoLog.v( 631 WM_SHELL_DESKTOP_MODE, 632 "DesktopTasksController: handleRequest result=%s", 633 result ?: "null" 634 ) 635 return result 636 } 637 638 /** 639 * Applies the proper surface states (rounded corners) to tasks when desktop mode is active. 640 * This is intended to be used when desktop mode is part of another animation but isn't, itself, 641 * animating. 642 */ 643 fun syncSurfaceState( 644 info: TransitionInfo, 645 finishTransaction: SurfaceControl.Transaction 646 ) { 647 // Add rounded corners to freeform windows 648 val ta: TypedArray = context.obtainStyledAttributes( 649 intArrayOf(R.attr.dialogCornerRadius)) 650 val cornerRadius = ta.getDimensionPixelSize(0, 0).toFloat() 651 ta.recycle() 652 info.changes 653 .filter { it.taskInfo?.windowingMode == WINDOWING_MODE_FREEFORM } 654 .forEach { finishTransaction.setCornerRadius(it.leash, cornerRadius) } 655 } 656 657 private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { 658 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") 659 val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) 660 if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { 661 KtProtoLog.d( 662 WM_SHELL_DESKTOP_MODE, 663 "DesktopTasksController: switch freeform task to fullscreen oon transition" + 664 " taskId=%d", 665 task.taskId 666 ) 667 return WindowContainerTransaction().also { wct -> 668 addMoveToFullscreenChanges(wct, task) 669 } 670 } 671 return null 672 } 673 674 private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { 675 KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") 676 val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) 677 if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { 678 KtProtoLog.d( 679 WM_SHELL_DESKTOP_MODE, 680 "DesktopTasksController: switch fullscreen task to freeform on transition" + 681 " taskId=%d", 682 task.taskId 683 ) 684 return WindowContainerTransaction().also { wct -> 685 addMoveToDesktopChanges(wct, task) 686 } 687 } 688 return null 689 } 690 691 private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction { 692 KtProtoLog.d( 693 WM_SHELL_DESKTOP_MODE, 694 "DesktopTasksController: launch apps with stashed on transition taskId=%d", 695 task.taskId 696 ) 697 val wct = WindowContainerTransaction() 698 bringDesktopAppsToFront(task.displayId, wct) 699 addMoveToDesktopChanges(wct, task) 700 desktopModeTaskRepository.setStashed(task.displayId, false) 701 return wct 702 } 703 704 private fun addMoveToDesktopChanges( 705 wct: WindowContainerTransaction, 706 taskInfo: RunningTaskInfo 707 ) { 708 val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode 709 val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FREEFORM) { 710 // Display windowing is freeform, set to undefined and inherit it 711 WINDOWING_MODE_UNDEFINED 712 } else { 713 WINDOWING_MODE_FREEFORM 714 } 715 wct.setWindowingMode(taskInfo.token, targetWindowingMode) 716 wct.reorder(taskInfo.token, true /* onTop */) 717 if (isDesktopDensityOverrideSet()) { 718 wct.setDensityDpi(taskInfo.token, getDesktopDensityDpi()) 719 } 720 } 721 722 private fun addMoveToFullscreenChanges( 723 wct: WindowContainerTransaction, 724 taskInfo: RunningTaskInfo 725 ) { 726 val displayWindowingMode = taskInfo.configuration.windowConfiguration.displayWindowingMode 727 val targetWindowingMode = if (displayWindowingMode == WINDOWING_MODE_FULLSCREEN) { 728 // Display windowing is fullscreen, set to undefined and inherit it 729 WINDOWING_MODE_UNDEFINED 730 } else { 731 WINDOWING_MODE_FULLSCREEN 732 } 733 wct.setWindowingMode(taskInfo.token, targetWindowingMode) 734 wct.setBounds(taskInfo.token, Rect()) 735 if (isDesktopDensityOverrideSet()) { 736 wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) 737 } 738 } 739 740 /** 741 * Adds split screen changes to a transaction. Note that bounds are not reset here due to 742 * animation; see {@link onDesktopSplitSelectAnimComplete} 743 */ 744 private fun addMoveToSplitChanges( 745 wct: WindowContainerTransaction, 746 taskInfo: RunningTaskInfo 747 ) { 748 wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW) 749 // The task's density may have been overridden in freeform; revert it here as we don't 750 // want it overridden in multi-window. 751 wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) 752 } 753 754 /** 755 * Requests a task be transitioned from desktop to split select. Applies needed windowing 756 * changes if this transition is enabled. 757 */ 758 fun requestSplit( 759 taskInfo: RunningTaskInfo 760 ) { 761 val windowingMode = taskInfo.windowingMode 762 if (windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM 763 ) { 764 val wct = WindowContainerTransaction() 765 addMoveToSplitChanges(wct, taskInfo) 766 splitScreenController.requestEnterSplitSelect(taskInfo, wct, 767 SPLIT_POSITION_BOTTOM_OR_RIGHT, taskInfo.configuration.windowConfiguration.bounds) 768 } 769 } 770 771 private fun getDefaultDensityDpi(): Int { 772 return context.resources.displayMetrics.densityDpi 773 } 774 775 private fun getDesktopDensityDpi(): Int { 776 return DESKTOP_DENSITY_OVERRIDE 777 } 778 779 /** Creates a new instance of the external interface to pass to another process. */ 780 private fun createExternalInterface(): ExternalInterfaceBinder { 781 return IDesktopModeImpl(this) 782 } 783 784 /** Get connection interface between sysui and shell */ 785 fun asDesktopMode(): DesktopMode { 786 return desktopMode 787 } 788 789 /** 790 * Perform checks required on drag move. Create/release fullscreen indicator as needed. 791 * Different sources for x and y coordinates are used due to different needs for each: 792 * We want split transitions to be based on input coordinates but fullscreen transition 793 * to be based on task edge coordinate. 794 * 795 * @param taskInfo the task being dragged. 796 * @param taskSurface SurfaceControl of dragged task. 797 * @param inputCoordinate coordinates of input. Used for checks against left/right edge of screen. 798 * @param taskBounds bounds of dragged task. Used for checks against status bar height. 799 */ 800 fun onDragPositioningMove( 801 taskInfo: RunningTaskInfo, 802 taskSurface: SurfaceControl, 803 inputCoordinate: PointF, 804 taskBounds: Rect 805 ) { 806 val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return 807 if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return 808 var type = DesktopModeVisualIndicator.determineIndicatorType(inputCoordinate, 809 taskBounds, displayLayout, context) 810 if (type != DesktopModeVisualIndicator.INVALID_INDICATOR && visualIndicator == null) { 811 visualIndicator = DesktopModeVisualIndicator( 812 syncQueue, taskInfo, 813 displayController, context, taskSurface, shellTaskOrganizer, 814 rootTaskDisplayAreaOrganizer, type) 815 visualIndicator?.createIndicatorWithAnimatedBounds() 816 return 817 } 818 if (visualIndicator?.eventOutsideRange(inputCoordinate.x, 819 taskBounds.top.toFloat()) == true) { 820 releaseVisualIndicator() 821 } 822 } 823 824 /** 825 * Perform checks required on drag end. Move to fullscreen if drag ends in status bar area. 826 * 827 * @param taskInfo the task being dragged. 828 * @param position position of surface when drag ends. 829 * @param inputCoordinate the coordinates of the motion event 830 * @param taskBounds the updated bounds of the task being dragged. 831 * @param windowDecor the window decoration for the task being dragged 832 */ 833 fun onDragPositioningEnd( 834 taskInfo: RunningTaskInfo, 835 position: Point, 836 inputCoordinate: PointF, 837 taskBounds: Rect, 838 windowDecor: DesktopModeWindowDecoration 839 ) { 840 if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { 841 return 842 } 843 if (taskBounds.top <= transitionAreaHeight) { 844 windowDecor.incrementRelayoutBlock() 845 moveToFullscreenWithAnimation(taskInfo, position) 846 } 847 if (inputCoordinate.x <= transitionAreaWidth) { 848 releaseVisualIndicator() 849 var wct = WindowContainerTransaction() 850 addMoveToSplitChanges(wct, taskInfo) 851 splitScreenController.requestEnterSplitSelect(taskInfo, wct, 852 SPLIT_POSITION_TOP_OR_LEFT, taskBounds) 853 } 854 if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width() 855 ?.minus(transitionAreaWidth) ?: return)) { 856 releaseVisualIndicator() 857 var wct = WindowContainerTransaction() 858 addMoveToSplitChanges(wct, taskInfo) 859 splitScreenController.requestEnterSplitSelect(taskInfo, wct, 860 SPLIT_POSITION_BOTTOM_OR_RIGHT, taskBounds) 861 } 862 } 863 864 /** 865 * Perform checks required on drag move. Create/release fullscreen indicator and transitions 866 * indicator to freeform or fullscreen dimensions as needed. 867 * 868 * @param taskInfo the task being dragged. 869 * @param taskSurface SurfaceControl of dragged task. 870 * @param y coordinate of dragged task. Used for checks against status bar height. 871 */ 872 fun onDragPositioningMoveThroughStatusBar( 873 taskInfo: RunningTaskInfo, 874 taskSurface: SurfaceControl, 875 y: Float 876 ) { 877 // If the motion event is above the status bar and the visual indicator is not yet visible, 878 // return since we do not need to show the visual indicator at this point. 879 if (y < getStatusBarHeight(taskInfo) && visualIndicator == null) { 880 return 881 } 882 if (visualIndicator == null) { 883 visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, 884 displayController, context, taskSurface, shellTaskOrganizer, 885 rootTaskDisplayAreaOrganizer, TO_DESKTOP_INDICATOR) 886 visualIndicator?.createIndicatorWithAnimatedBounds() 887 } 888 val indicator = visualIndicator ?: return 889 if (y >= getFreeformTransitionStatusBarDragThreshold(taskInfo)) { 890 if (indicator.isFullscreen) { 891 indicator.transitionFullscreenIndicatorToFreeform() 892 } 893 } else if (!indicator.isFullscreen) { 894 indicator.transitionFreeformIndicatorToFullscreen() 895 } 896 } 897 898 /** 899 * Perform checks required when drag ends under status bar area. 900 * 901 * @param taskInfo the task being dragged. 902 * @param y height of drag, to be checked against status bar height. 903 */ 904 fun onDragPositioningEndThroughStatusBar( 905 taskInfo: RunningTaskInfo, 906 freeformBounds: Rect 907 ) { 908 finalizeMoveToDesktop(taskInfo, freeformBounds) 909 } 910 911 private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int { 912 return displayController.getDisplayLayout(taskInfo.displayId)?.stableInsets()?.top ?: 0 913 } 914 915 /** 916 * Returns the threshold at which we transition a task into freeform when dragging a 917 * fullscreen task down from the status bar 918 */ 919 private fun getFreeformTransitionStatusBarDragThreshold(taskInfo: RunningTaskInfo): Int { 920 return 2 * getStatusBarHeight(taskInfo) 921 } 922 923 /** 924 * Update the corner region for a specified task 925 */ 926 fun onTaskCornersChanged(taskId: Int, corner: Region) { 927 desktopModeTaskRepository.updateTaskCorners(taskId, corner) 928 } 929 930 /** 931 * Remove a previously tracked corner region for a specified task. 932 */ 933 fun removeCornersForTask(taskId: Int) { 934 desktopModeTaskRepository.removeTaskCorners(taskId) 935 } 936 937 /** 938 * Adds a listener to find out about changes in the visibility of freeform tasks. 939 * 940 * @param listener the listener to add. 941 * @param callbackExecutor the executor to call the listener on. 942 */ 943 fun addVisibleTasksListener(listener: VisibleTasksListener, callbackExecutor: Executor) { 944 desktopModeTaskRepository.addVisibleTasksListener(listener, callbackExecutor) 945 } 946 947 /** 948 * Adds a listener to track changes to desktop task corners 949 * 950 * @param listener the listener to add. 951 * @param callbackExecutor the executor to call the listener on. 952 */ 953 fun setTaskCornerListener( 954 listener: Consumer<Region>, 955 callbackExecutor: Executor 956 ) { 957 desktopModeTaskRepository.setTaskCornerListener(listener, callbackExecutor) 958 } 959 960 private fun dump(pw: PrintWriter, prefix: String) { 961 val innerPrefix = "$prefix " 962 pw.println("${prefix}DesktopTasksController") 963 desktopModeTaskRepository.dump(pw, innerPrefix) 964 } 965 966 /** The interface for calls from outside the shell, within the host process. */ 967 @ExternalThread 968 private inner class DesktopModeImpl : DesktopMode { 969 override fun addVisibleTasksListener( 970 listener: VisibleTasksListener, 971 callbackExecutor: Executor 972 ) { 973 mainExecutor.execute { 974 this@DesktopTasksController.addVisibleTasksListener(listener, callbackExecutor) 975 } 976 } 977 978 override fun addDesktopGestureExclusionRegionListener( 979 listener: Consumer<Region>, 980 callbackExecutor: Executor 981 ) { 982 mainExecutor.execute { 983 this@DesktopTasksController.setTaskCornerListener(listener, callbackExecutor) 984 } 985 } 986 } 987 988 /** The interface for calls from outside the host process. */ 989 @BinderThread 990 private class IDesktopModeImpl(private var controller: DesktopTasksController?) : 991 IDesktopMode.Stub(), ExternalInterfaceBinder { 992 993 private lateinit var remoteListener: 994 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> 995 996 private val listener: VisibleTasksListener = object : VisibleTasksListener { 997 override fun onVisibilityChanged(displayId: Int, visible: Boolean) { 998 KtProtoLog.v( 999 WM_SHELL_DESKTOP_MODE, 1000 "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b", 1001 displayId, 1002 visible 1003 ) 1004 remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) } 1005 } 1006 1007 override fun onStashedChanged(displayId: Int, stashed: Boolean) { 1008 KtProtoLog.v( 1009 WM_SHELL_DESKTOP_MODE, 1010 "IDesktopModeImpl: onStashedChanged display=%d stashed=%b", 1011 displayId, 1012 stashed 1013 ) 1014 remoteListener.call { l -> l.onStashedChanged(displayId, stashed) } 1015 } 1016 } 1017 1018 init { 1019 remoteListener = 1020 SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener>( 1021 controller, 1022 { c -> 1023 c.desktopModeTaskRepository.addVisibleTasksListener( 1024 listener, 1025 c.mainExecutor 1026 ) 1027 }, 1028 { c -> 1029 c.desktopModeTaskRepository.removeVisibleTasksListener(listener) 1030 } 1031 ) 1032 } 1033 1034 /** Invalidates this instance, preventing future calls from updating the controller. */ 1035 override fun invalidate() { 1036 remoteListener.unregister() 1037 controller = null 1038 } 1039 1040 override fun showDesktopApps(displayId: Int) { 1041 ExecutorUtils.executeRemoteCallWithTaskPermission( 1042 controller, 1043 "showDesktopApps" 1044 ) { c -> c.showDesktopApps(displayId) } 1045 } 1046 1047 override fun stashDesktopApps(displayId: Int) { 1048 ExecutorUtils.executeRemoteCallWithTaskPermission( 1049 controller, 1050 "stashDesktopApps" 1051 ) { c -> c.stashDesktopApps(displayId) } 1052 } 1053 1054 override fun hideStashedDesktopApps(displayId: Int) { 1055 ExecutorUtils.executeRemoteCallWithTaskPermission( 1056 controller, 1057 "hideStashedDesktopApps" 1058 ) { c -> c.hideStashedDesktopApps(displayId) } 1059 } 1060 1061 override fun showDesktopApp(taskId: Int) { 1062 ExecutorUtils.executeRemoteCallWithTaskPermission( 1063 controller, 1064 "showDesktopApp" 1065 ) { c -> c.moveTaskToFront(taskId) } 1066 } 1067 1068 override fun getVisibleTaskCount(displayId: Int): Int { 1069 val result = IntArray(1) 1070 ExecutorUtils.executeRemoteCallWithTaskPermission( 1071 controller, 1072 "getVisibleTaskCount", 1073 { controller -> result[0] = controller.getVisibleTaskCount(displayId) }, 1074 true /* blocking */ 1075 ) 1076 return result[0] 1077 } 1078 1079 override fun onDesktopSplitSelectAnimComplete(taskInfo: RunningTaskInfo) { 1080 ExecutorUtils.executeRemoteCallWithTaskPermission( 1081 controller, 1082 "onDesktopSplitSelectAnimComplete" 1083 ) { c -> c.onDesktopSplitSelectAnimComplete(taskInfo) } 1084 } 1085 1086 override fun setTaskListener(listener: IDesktopTaskListener?) { 1087 KtProtoLog.v( 1088 WM_SHELL_DESKTOP_MODE, 1089 "IDesktopModeImpl: set task listener=%s", 1090 listener ?: "null" 1091 ) 1092 ExecutorUtils.executeRemoteCallWithTaskPermission( 1093 controller, 1094 "setTaskListener" 1095 ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } 1096 } 1097 } 1098 1099 companion object { 1100 private val DESKTOP_DENSITY_OVERRIDE = 1101 SystemProperties.getInt("persist.wm.debug.desktop_mode_density", 284) 1102 private val DESKTOP_DENSITY_ALLOWED_RANGE = (100..1000) 1103 1104 // Override default freeform task width when desktop mode is enabled. In dips. 1105 private val DESKTOP_MODE_DEFAULT_WIDTH_DP = 1106 SystemProperties.getInt("persist.wm.debug.desktop_mode.default_width", 840) 1107 1108 // Override default freeform task height when desktop mode is enabled. In dips. 1109 private val DESKTOP_MODE_DEFAULT_HEIGHT_DP = 1110 SystemProperties.getInt("persist.wm.debug.desktop_mode.default_height", 630) 1111 1112 /** 1113 * Check if desktop density override is enabled 1114 */ 1115 @JvmStatic 1116 fun isDesktopDensityOverrideSet(): Boolean { 1117 return DESKTOP_DENSITY_OVERRIDE in DESKTOP_DENSITY_ALLOWED_RANGE 1118 } 1119 } 1120 } 1121