1 /* 2 * 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.windowdecor.tiling 18 19 import android.app.ActivityManager.RunningTaskInfo 20 import android.app.WindowConfiguration.WINDOWING_MODE_PINNED 21 import android.content.Context 22 import android.content.res.Configuration 23 import android.content.res.Resources 24 import android.graphics.Rect 25 import android.os.IBinder 26 import android.os.UserHandle 27 import android.view.MotionEvent 28 import android.view.SurfaceControl 29 import android.view.SurfaceControl.Transaction 30 import android.view.WindowManager.TRANSIT_CHANGE 31 import android.view.WindowManager.TRANSIT_OPEN 32 import android.view.WindowManager.TRANSIT_PIP 33 import android.view.WindowManager.TRANSIT_TO_BACK 34 import android.view.WindowManager.TRANSIT_TO_FRONT 35 import android.window.TransitionInfo 36 import android.window.TransitionInfo.Change 37 import android.window.TransitionRequestInfo 38 import android.window.WindowContainerTransaction 39 import com.android.internal.annotations.VisibleForTesting 40 import com.android.launcher3.icons.BaseIconFactory 41 import com.android.window.flags.Flags 42 import com.android.wm.shell.R 43 import com.android.wm.shell.RootTaskDisplayAreaOrganizer 44 import com.android.wm.shell.ShellTaskOrganizer 45 import com.android.wm.shell.common.DisplayController 46 import com.android.wm.shell.common.DisplayLayout 47 import com.android.wm.shell.common.ShellExecutor 48 import com.android.wm.shell.common.SyncTransactionQueue 49 import com.android.wm.shell.desktopmode.DesktopModeEventLogger 50 import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger 51 import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition 52 import com.android.wm.shell.desktopmode.DesktopUserRepositories 53 import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator 54 import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler 55 import com.android.wm.shell.shared.FocusTransitionListener 56 import com.android.wm.shell.shared.annotations.ShellBackgroundThread 57 import com.android.wm.shell.shared.annotations.ShellMainThread 58 import com.android.wm.shell.transition.FocusTransitionObserver 59 import com.android.wm.shell.transition.Transitions 60 import com.android.wm.shell.transition.Transitions.TRANSIT_MINIMIZE 61 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration 62 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility 63 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener 64 import com.android.wm.shell.windowdecor.DragResizeWindowGeometry 65 import com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE 66 import com.android.wm.shell.windowdecor.ResizeVeil 67 import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader 68 import com.android.wm.shell.windowdecor.extension.isFullscreen 69 import java.util.function.Supplier 70 import kotlinx.coroutines.CoroutineScope 71 import kotlinx.coroutines.MainCoroutineDispatcher 72 73 class DesktopTilingWindowDecoration( 74 private var context: Context, 75 @ShellMainThread private val mainDispatcher: MainCoroutineDispatcher, 76 @ShellBackgroundThread private val bgScope: CoroutineScope, 77 private val syncQueue: SyncTransactionQueue, 78 private val displayController: DisplayController, 79 private val taskResourceLoader: WindowDecorTaskResourceLoader, 80 private val displayId: Int, 81 private val rootTdaOrganizer: RootTaskDisplayAreaOrganizer, 82 private val transitions: Transitions, 83 private val shellTaskOrganizer: ShellTaskOrganizer, 84 private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, 85 private val returnToDragStartAnimator: ReturnToDragStartAnimator, 86 private val desktopUserRepositories: DesktopUserRepositories, 87 private val desktopModeEventLogger: DesktopModeEventLogger, 88 private val focusTransitionObserver: FocusTransitionObserver, 89 @ShellMainThread private val mainExecutor: ShellExecutor, <lambda>null90 private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() }, 91 ) : 92 Transitions.TransitionHandler, 93 ShellTaskOrganizer.FocusListener, 94 ShellTaskOrganizer.TaskVanishedListener, 95 DragEventListener, 96 Transitions.TransitionObserver, 97 FocusTransitionListener { 98 companion object { 99 private val TAG: String = DesktopTilingWindowDecoration::class.java.simpleName 100 private const val TILING_DIVIDER_TAG = "Tiling Divider" 101 } 102 103 var leftTaskResizingHelper: AppResizingHelper? = null 104 var rightTaskResizingHelper: AppResizingHelper? = null 105 private var isTilingManagerInitialised = false 106 @VisibleForTesting 107 var desktopTilingDividerWindowManager: DesktopTilingDividerWindowManager? = null 108 private lateinit var dividerBounds: Rect 109 private var isDarkMode = false 110 private var isResizing = false 111 private var isTilingFocused = false 112 onAppTilednull113 fun onAppTiled( 114 taskInfo: RunningTaskInfo, 115 desktopModeWindowDecoration: DesktopModeWindowDecoration, 116 position: SnapPosition, 117 currentBounds: Rect, 118 ): Boolean { 119 val destinationBounds = getSnapBounds(position) 120 val resizeMetadata = 121 AppResizingHelper( 122 taskInfo, 123 desktopModeWindowDecoration, 124 context, 125 destinationBounds, 126 displayController, 127 taskResourceLoader, 128 mainDispatcher, 129 bgScope, 130 transactionSupplier, 131 ) 132 val isFirstTiledApp = leftTaskResizingHelper == null && rightTaskResizingHelper == null 133 val isTiled = destinationBounds != taskInfo.configuration.windowConfiguration.bounds 134 135 initTilingApps(resizeMetadata, position, taskInfo) 136 isDarkMode = isTaskInDarkMode(taskInfo) 137 // Observe drag resizing to break tiling if a task is drag resized. 138 desktopModeWindowDecoration.addDragResizeListener(this) 139 val callback = { initTilingForDisplayIfNeeded(taskInfo.configuration, isFirstTiledApp) } 140 updateDesktopRepository(taskInfo.taskId, snapPosition = position) 141 if (isTiled) { 142 val wct = WindowContainerTransaction().setBounds(taskInfo.token, destinationBounds) 143 toggleResizeDesktopTaskTransitionHandler.startTransition(wct, currentBounds, callback) 144 } else { 145 // Handle the case where we attempt to snap resize when already snap resized: the task 146 // position won't need to change but we want to animate the surface going back to the 147 // snapped position from the "dragged-to-the-edge" position. 148 if (destinationBounds != currentBounds) { 149 returnToDragStartAnimator.start( 150 taskInfo.taskId, 151 resizeMetadata.getLeash(), 152 startBounds = currentBounds, 153 endBounds = destinationBounds, 154 callback, 155 ) 156 } else { 157 callback.invoke() 158 } 159 } 160 return isTiled 161 } 162 updateDesktopRepositorynull163 private fun updateDesktopRepository(taskId: Int, snapPosition: SnapPosition) { 164 when (snapPosition) { 165 SnapPosition.LEFT -> desktopUserRepositories.current.addLeftTiledTask(displayId, taskId) 166 SnapPosition.RIGHT -> 167 desktopUserRepositories.current.addRightTiledTask(displayId, taskId) 168 } 169 } 170 171 // If a task is already tiled on the same position, release this task, otherwise if the same 172 // task is tiled on the opposite side, remove it from the opposite side so it's tiled correctly. initTilingAppsnull173 private fun initTilingApps( 174 taskResizingHelper: AppResizingHelper, 175 position: SnapPosition, 176 taskInfo: RunningTaskInfo, 177 ) { 178 when (position) { 179 SnapPosition.RIGHT -> { 180 rightTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) } 181 if (leftTaskResizingHelper?.taskInfo?.taskId == taskInfo.taskId) { 182 removeTaskIfTiled(taskInfo.taskId) 183 } 184 rightTaskResizingHelper = taskResizingHelper 185 } 186 187 SnapPosition.LEFT -> { 188 leftTaskResizingHelper?.let { removeTaskIfTiled(it.taskInfo.taskId) } 189 if (taskInfo.taskId == rightTaskResizingHelper?.taskInfo?.taskId) { 190 removeTaskIfTiled(taskInfo.taskId) 191 } 192 leftTaskResizingHelper = taskResizingHelper 193 } 194 } 195 } 196 initTilingForDisplayIfNeedednull197 private fun initTilingForDisplayIfNeeded(config: Configuration, firstTiledApp: Boolean) { 198 if (leftTaskResizingHelper != null && rightTaskResizingHelper != null) { 199 if (!isTilingManagerInitialised) { 200 desktopTilingDividerWindowManager = initTilingManagerForDisplay(displayId, config) 201 isTilingManagerInitialised = true 202 203 if (Flags.enableDisplayFocusInShellTransitions()) { 204 focusTransitionObserver.setLocalFocusTransitionListener(this, mainExecutor) 205 } else { 206 shellTaskOrganizer.addFocusListener(this) 207 isTilingFocused = true 208 } 209 } 210 leftTaskResizingHelper?.initIfNeeded() 211 rightTaskResizingHelper?.initIfNeeded() 212 leftTaskResizingHelper 213 ?.desktopModeWindowDecoration 214 ?.updateDisabledResizingEdge( 215 DragResizeWindowGeometry.DisabledEdge.RIGHT, 216 /* shouldDelayUpdate = */ false, 217 ) 218 rightTaskResizingHelper 219 ?.desktopModeWindowDecoration 220 ?.updateDisabledResizingEdge( 221 DragResizeWindowGeometry.DisabledEdge.LEFT, 222 /* shouldDelayUpdate = */ false, 223 ) 224 } else if (firstTiledApp) { 225 shellTaskOrganizer.addTaskVanishedListener(this) 226 } 227 } 228 initTilingManagerForDisplaynull229 private fun initTilingManagerForDisplay( 230 displayId: Int, 231 config: Configuration, 232 ): DesktopTilingDividerWindowManager? { 233 val displayLayout = displayController.getDisplayLayout(displayId) 234 val builder = SurfaceControl.Builder() 235 rootTdaOrganizer.attachToDisplayArea(displayId, builder) 236 val leash = builder.setName(TILING_DIVIDER_TAG).setContainerLayer().build() 237 val displayContext = displayController.getDisplayContext(displayId) ?: return null 238 val tilingManager = 239 displayLayout?.let { 240 dividerBounds = inflateDividerBounds(it) 241 DesktopTilingDividerWindowManager( 242 config, 243 TAG, 244 context, 245 leash, 246 syncQueue, 247 this, 248 transactionSupplier, 249 dividerBounds, 250 displayContext, 251 isDarkMode, 252 ) 253 } 254 // a leash to present the divider on top of, without re-parenting. 255 val relativeLeash = 256 leftTaskResizingHelper?.desktopModeWindowDecoration?.getLeash() ?: return tilingManager 257 tilingManager?.generateViewHost(relativeLeash) 258 return tilingManager 259 } 260 onDividerHandleDragStartnull261 fun onDividerHandleDragStart(motionEvent: MotionEvent) { 262 val leftTiledTask = leftTaskResizingHelper ?: return 263 val rightTiledTask = rightTaskResizingHelper ?: return 264 val inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent) 265 266 desktopModeEventLogger.logTaskResizingStarted( 267 ResizeTrigger.TILING_DIVIDER, 268 inputMethod, 269 leftTiledTask.taskInfo, 270 leftTiledTask.bounds.width(), 271 leftTiledTask.bounds.height(), 272 displayController, 273 ) 274 275 desktopModeEventLogger.logTaskResizingStarted( 276 ResizeTrigger.TILING_DIVIDER, 277 inputMethod, 278 rightTiledTask.taskInfo, 279 rightTiledTask.bounds.width(), 280 rightTiledTask.bounds.height(), 281 displayController, 282 ) 283 } 284 onDividerHandleMovednull285 fun onDividerHandleMoved(dividerBounds: Rect, t: SurfaceControl.Transaction): Boolean { 286 val leftTiledTask = leftTaskResizingHelper ?: return false 287 val rightTiledTask = rightTaskResizingHelper ?: return false 288 val stableBounds = Rect() 289 val displayLayout = displayController.getDisplayLayout(displayId) 290 displayLayout?.getStableBounds(stableBounds) 291 292 if (stableBounds.isEmpty) return false 293 294 val leftBounds = leftTiledTask.bounds 295 val rightBounds = rightTiledTask.bounds 296 val newLeftBounds = 297 Rect(leftBounds.left, leftBounds.top, dividerBounds.left, leftBounds.bottom) 298 val newRightBounds = 299 Rect(dividerBounds.right, rightBounds.top, rightBounds.right, rightBounds.bottom) 300 301 // If one of the apps is getting smaller or bigger than size constraint, ignore finger move. 302 if ( 303 isResizeWithinSizeConstraints( 304 newLeftBounds, 305 newRightBounds, 306 leftBounds, 307 rightBounds, 308 stableBounds, 309 ) 310 ) { 311 return false 312 } 313 314 // The final new bounds for each app has to be registered to make sure a startAnimate 315 // when the new bounds are different from old bounds, otherwise hide the veil without 316 // waiting for an animation as no animation will run when no bounds are changed. 317 leftTiledTask.newBounds.set(newLeftBounds) 318 rightTiledTask.newBounds.set(newRightBounds) 319 if (!isResizing) { 320 leftTiledTask.showVeil(t) 321 rightTiledTask.showVeil(t) 322 isResizing = true 323 } else { 324 leftTiledTask.updateVeil(t) 325 rightTiledTask.updateVeil(t) 326 } 327 328 // Applies showing/updating veil for both apps and moving the divider into its new position. 329 t.apply() 330 return true 331 } 332 onDividerHandleDragEndnull333 fun onDividerHandleDragEnd( 334 dividerBounds: Rect, 335 t: SurfaceControl.Transaction, 336 motionEvent: MotionEvent, 337 ) { 338 val leftTiledTask = leftTaskResizingHelper ?: return 339 val rightTiledTask = rightTaskResizingHelper ?: return 340 val inputMethod = DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent) 341 342 desktopModeEventLogger.logTaskResizingEnded( 343 ResizeTrigger.TILING_DIVIDER, 344 inputMethod, 345 leftTiledTask.taskInfo, 346 leftTiledTask.newBounds.width(), 347 leftTiledTask.newBounds.height(), 348 displayController, 349 ) 350 351 desktopModeEventLogger.logTaskResizingEnded( 352 ResizeTrigger.TILING_DIVIDER, 353 inputMethod, 354 rightTiledTask.taskInfo, 355 rightTiledTask.newBounds.width(), 356 rightTiledTask.newBounds.height(), 357 displayController, 358 ) 359 360 if (leftTiledTask.newBounds == leftTiledTask.bounds) { 361 leftTiledTask.hideVeil() 362 rightTiledTask.hideVeil() 363 isResizing = false 364 return 365 } 366 leftTiledTask.bounds.set(leftTiledTask.newBounds) 367 rightTiledTask.bounds.set(rightTiledTask.newBounds) 368 onDividerHandleMoved(dividerBounds, t) 369 isResizing = false 370 val wct = WindowContainerTransaction() 371 wct.setBounds(leftTiledTask.taskInfo.token, leftTiledTask.bounds) 372 wct.setBounds(rightTiledTask.taskInfo.token, rightTiledTask.bounds) 373 transitions.startTransition(TRANSIT_CHANGE, wct, this) 374 } 375 onTaskInfoChangenull376 fun onTaskInfoChange(taskInfo: RunningTaskInfo) { 377 val isCurrentTaskInDarkMode = isTaskInDarkMode(taskInfo) 378 desktopTilingDividerWindowManager?.onTaskInfoChange() 379 if (isCurrentTaskInDarkMode == isDarkMode || !isTilingManagerInitialised) return 380 isDarkMode = isCurrentTaskInDarkMode 381 desktopTilingDividerWindowManager?.onUiModeChange(isDarkMode) 382 } 383 isTaskInDarkModenull384 fun isTaskInDarkMode(taskInfo: RunningTaskInfo): Boolean = 385 (taskInfo.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == 386 Configuration.UI_MODE_NIGHT_YES 387 388 override fun startAnimation( 389 transition: IBinder, 390 info: TransitionInfo, 391 startTransaction: Transaction, 392 finishTransaction: Transaction, 393 finishCallback: Transitions.TransitionFinishCallback, 394 ): Boolean { 395 val leftTiledTask = leftTaskResizingHelper ?: return false 396 val rightTiledTask = rightTaskResizingHelper ?: return false 397 for (change in info.getChanges()) { 398 val sc: SurfaceControl = change.getLeash() 399 val endBounds = 400 if (change.taskInfo?.taskId == leftTiledTask.taskInfo.taskId) { 401 leftTiledTask.bounds 402 } else { 403 rightTiledTask.bounds 404 } 405 startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) 406 finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) 407 } 408 409 startTransaction.apply() 410 leftTiledTask.hideVeil() 411 rightTiledTask.hideVeil() 412 finishCallback.onTransitionFinished(null) 413 return true 414 } 415 416 // TODO(b/361505243) bring tasks to front here when the empty request info bug is fixed. handleRequestnull417 override fun handleRequest( 418 transition: IBinder, 419 request: TransitionRequestInfo, 420 ): WindowContainerTransaction? { 421 return null 422 } 423 onDragStartnull424 override fun onDragStart(taskId: Int) {} 425 onDragMovenull426 override fun onDragMove(taskId: Int) { 427 removeTaskIfTiled(taskId) 428 } 429 onTransitionReadynull430 override fun onTransitionReady( 431 transition: IBinder, 432 info: TransitionInfo, 433 startTransaction: Transaction, 434 finishTransaction: Transaction, 435 ) { 436 var leftTaskBroughtToFront = false 437 var rightTaskBroughtToFront = false 438 439 for (change in info.changes) { 440 change.taskInfo?.let { 441 if (it.isFullscreen || isMinimized(change.mode, info.type)) { 442 removeTaskIfTiled(it.taskId, /* taskVanished= */ false, it.isFullscreen) 443 } else if (isEnteringPip(change, info.type)) { 444 removeTaskIfTiled(it.taskId, /* taskVanished= */ true, it.isFullscreen) 445 } else if (isTransitionToFront(change.mode, info.type)) { 446 handleTaskBroughtToFront(it.taskId) 447 leftTaskBroughtToFront = 448 leftTaskBroughtToFront || 449 it.taskId == leftTaskResizingHelper?.taskInfo?.taskId 450 rightTaskBroughtToFront = 451 rightTaskBroughtToFront || 452 it.taskId == rightTaskResizingHelper?.taskInfo?.taskId 453 } 454 } 455 } 456 457 if (leftTaskBroughtToFront && rightTaskBroughtToFront) { 458 desktopTilingDividerWindowManager?.showDividerBar() 459 } 460 } 461 handleTaskBroughtToFrontnull462 private fun handleTaskBroughtToFront(taskId: Int) { 463 if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { 464 leftTaskResizingHelper?.onAppBecomingVisible() 465 } else if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { 466 rightTaskResizingHelper?.onAppBecomingVisible() 467 } 468 } 469 isMinimizednull470 private fun isMinimized(changeMode: Int, infoType: Int): Boolean { 471 return (changeMode == TRANSIT_TO_BACK && 472 (infoType == TRANSIT_MINIMIZE || 473 infoType == TRANSIT_TO_BACK || 474 infoType == TRANSIT_OPEN)) 475 } 476 isEnteringPipnull477 private fun isEnteringPip(change: Change, transitType: Int): Boolean { 478 if (change.taskInfo != null && change.taskInfo?.windowingMode == WINDOWING_MODE_PINNED) { 479 // - TRANSIT_PIP: type (from RootWindowContainer) 480 // - TRANSIT_OPEN (from apps that enter PiP instantly on opening, mostly from 481 // CTS/Flicker tests). 482 // - TRANSIT_TO_FRONT, though uncommon with triggering PiP, should semantically also 483 // be allowed to animate if the task in question is pinned already - see b/308054074. 484 // - TRANSIT_CHANGE: This can happen if the request to enter PIP happens when we are 485 // collecting for another transition, such as TRANSIT_CHANGE (display rotation). 486 if ( 487 transitType == TRANSIT_PIP || 488 transitType == TRANSIT_OPEN || 489 transitType == TRANSIT_TO_FRONT || 490 transitType == TRANSIT_CHANGE 491 ) { 492 return true 493 } 494 } 495 return false 496 } 497 isTransitionToFrontnull498 private fun isTransitionToFront(changeMode: Int, transitionType: Int): Boolean = 499 changeMode == TRANSIT_TO_FRONT && transitionType == TRANSIT_TO_FRONT 500 501 class AppResizingHelper( 502 val taskInfo: RunningTaskInfo, 503 val desktopModeWindowDecoration: DesktopModeWindowDecoration, 504 val context: Context, 505 val bounds: Rect, 506 val displayController: DisplayController, 507 private val taskResourceLoader: WindowDecorTaskResourceLoader, 508 @ShellMainThread val mainDispatcher: MainCoroutineDispatcher, 509 @ShellBackgroundThread val bgScope: CoroutineScope, 510 val transactionSupplier: Supplier<Transaction>, 511 ) { 512 var isInitialised = false 513 var newBounds = Rect(bounds) 514 var visibilityCallback: (() -> Unit)? = null 515 private lateinit var resizeVeil: ResizeVeil 516 private val displayContext = displayController.getDisplayContext(taskInfo.displayId) 517 private val userContext = 518 context.createContextAsUser(UserHandle.of(taskInfo.userId), /* flags= */ 0) 519 520 fun initIfNeeded() { 521 if (!isInitialised) { 522 initVeil() 523 isInitialised = true 524 } 525 } 526 527 private fun initVeil() { 528 displayContext ?: return 529 resizeVeil = 530 ResizeVeil( 531 context = displayContext, 532 displayController = displayController, 533 taskResourceLoader = taskResourceLoader, 534 mainDispatcher = mainDispatcher, 535 bgScope = bgScope, 536 parentSurface = desktopModeWindowDecoration.getLeash(), 537 surfaceControlTransactionSupplier = transactionSupplier, 538 taskInfo = taskInfo, 539 ) 540 } 541 542 fun showVeil(t: Transaction) = 543 resizeVeil.updateTransactionWithShowVeil( 544 t, 545 desktopModeWindowDecoration.getLeash(), 546 bounds, 547 taskInfo, 548 ) 549 550 fun updateVeil(t: Transaction) = resizeVeil.updateTransactionWithResizeVeil(t, newBounds) 551 552 fun onAppBecomingVisible() { 553 visibilityCallback?.invoke() 554 visibilityCallback = null 555 } 556 557 fun hideVeil() = resizeVeil.hideVeil() 558 559 private fun createIconFactory(context: Context, dimensions: Int): BaseIconFactory { 560 val resources: Resources = context.resources 561 val densityDpi: Int = resources.getDisplayMetrics().densityDpi 562 val iconSize: Int = resources.getDimensionPixelSize(dimensions) 563 return BaseIconFactory(context, densityDpi, iconSize) 564 } 565 566 fun getLeash(): SurfaceControl = desktopModeWindowDecoration.getLeash() 567 568 fun dispose() { 569 if (isInitialised) resizeVeil.dispose() 570 } 571 } 572 573 // Only called if [taskId] relates to a focused task isTilingFocusRemovednull574 private fun isTilingFocusRemoved(taskId: Int): Boolean { 575 return isTilingFocused && 576 taskId != leftTaskResizingHelper?.taskInfo?.taskId && 577 taskId != rightTaskResizingHelper?.taskInfo?.taskId 578 } 579 580 // Overriding ShellTaskOrganizer.FocusListener onFocusTaskChangednull581 override fun onFocusTaskChanged(taskInfo: RunningTaskInfo?) { 582 if (Flags.enableDisplayFocusInShellTransitions()) return 583 if (taskInfo != null) { 584 moveTiledPairToFront(taskInfo.taskId, taskInfo.isFocused) 585 } 586 } 587 588 // Overriding FocusTransitionListener onFocusedTaskChangednull589 override fun onFocusedTaskChanged( 590 taskId: Int, 591 isFocusedOnDisplay: Boolean, 592 isFocusedGlobally: Boolean, 593 ) { 594 if (!Flags.enableDisplayFocusInShellTransitions()) return 595 moveTiledPairToFront(taskId, isFocusedOnDisplay) 596 } 597 598 // Only called if [taskInfo] relates to a focused task isTilingRefocusednull599 private fun isTilingRefocused(taskId: Int): Boolean { 600 return taskId == leftTaskResizingHelper?.taskInfo?.taskId || 601 taskId == rightTaskResizingHelper?.taskInfo?.taskId 602 } 603 buildTiledTasksMoveToFrontnull604 private fun buildTiledTasksMoveToFront(leftOnTop: Boolean): WindowContainerTransaction { 605 val wct = WindowContainerTransaction() 606 val leftTiledTask = leftTaskResizingHelper ?: return wct 607 val rightTiledTask = rightTaskResizingHelper ?: return wct 608 if (leftOnTop) { 609 wct.reorder(rightTiledTask.taskInfo.token, true) 610 wct.reorder(leftTiledTask.taskInfo.token, true) 611 } else { 612 wct.reorder(leftTiledTask.taskInfo.token, true) 613 wct.reorder(rightTiledTask.taskInfo.token, true) 614 } 615 return wct 616 } 617 removeTaskIfTilednull618 fun removeTaskIfTiled( 619 taskId: Int, 620 taskVanished: Boolean = false, 621 shouldDelayUpdate: Boolean = false, 622 ) { 623 val taskRepository = desktopUserRepositories.current 624 if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { 625 desktopUserRepositories.current.removeLeftTiledTask(displayId) 626 removeTask(leftTaskResizingHelper, taskVanished, shouldDelayUpdate) 627 leftTaskResizingHelper = null 628 val taskId = rightTaskResizingHelper?.taskInfo?.taskId 629 val callback: (() -> Unit)? = { 630 rightTaskResizingHelper 631 ?.desktopModeWindowDecoration 632 ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) 633 } 634 if (taskId != null && taskRepository.isVisibleTask(taskId)) { 635 callback?.invoke() 636 } else if (rightTaskResizingHelper != null) { 637 rightTaskResizingHelper?.visibilityCallback = callback 638 } 639 tearDownTiling() 640 return 641 } 642 643 if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { 644 desktopUserRepositories.current.removeRightTiledTask(displayId) 645 removeTask(rightTaskResizingHelper, taskVanished, shouldDelayUpdate) 646 rightTaskResizingHelper = null 647 val taskId = leftTaskResizingHelper?.taskInfo?.taskId 648 val callback: (() -> Unit)? = { 649 leftTaskResizingHelper 650 ?.desktopModeWindowDecoration 651 ?.updateDisabledResizingEdge(NONE, shouldDelayUpdate) 652 } 653 if (taskId != null && taskRepository.isVisibleTask(taskId)) { 654 callback?.invoke() 655 } else if (leftTaskResizingHelper != null) { 656 leftTaskResizingHelper?.visibilityCallback = callback 657 } 658 659 tearDownTiling() 660 } 661 } 662 resetTilingSessionnull663 fun resetTilingSession() { 664 if (leftTaskResizingHelper != null) { 665 removeTask(leftTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true) 666 leftTaskResizingHelper = null 667 } 668 if (rightTaskResizingHelper != null) { 669 removeTask(rightTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true) 670 rightTaskResizingHelper = null 671 } 672 tearDownTiling() 673 } 674 removeTasknull675 private fun removeTask( 676 appResizingHelper: AppResizingHelper?, 677 taskVanished: Boolean = false, 678 shouldDelayUpdate: Boolean, 679 ) { 680 if (appResizingHelper == null) return 681 if (!taskVanished) { 682 appResizingHelper.desktopModeWindowDecoration.removeDragResizeListener(this) 683 appResizingHelper.desktopModeWindowDecoration.updateDisabledResizingEdge( 684 NONE, 685 shouldDelayUpdate, 686 ) 687 } 688 appResizingHelper.dispose() 689 } 690 onOverviewAnimationStateChangenull691 fun onOverviewAnimationStateChange(isRunning: Boolean) { 692 if (!isTilingManagerInitialised) return 693 if (isRunning) { 694 desktopTilingDividerWindowManager?.hideDividerBar() 695 } else if (allTiledTasksVisible()) { 696 desktopTilingDividerWindowManager?.showDividerBar() 697 } 698 } 699 onTaskVanishednull700 override fun onTaskVanished(taskInfo: RunningTaskInfo?) { 701 val taskId = taskInfo?.taskId ?: return 702 removeTaskIfTiled(taskId, taskVanished = true, shouldDelayUpdate = true) 703 } 704 705 /** 706 * Moves the tiled pair to the front of the task stack, if the [taskInfo] is focused and one of 707 * the two tiled tasks. 708 * 709 * If specified, [isTaskFocused] will override [RunningTaskInfo.isFocused]. This is to be used 710 * when called when the task will be focused, but the [taskInfo] hasn't been updated yet. 711 */ moveTiledPairToFrontnull712 fun moveTiledPairToFront(taskId: Int, isFocusedOnDisplay: Boolean): Boolean { 713 if (!isTilingManagerInitialised) return false 714 715 if (!isFocusedOnDisplay) return false 716 717 // If a task that isn't tiled is being focused, let the generic handler do the work. 718 if (!Flags.enableDisplayFocusInShellTransitions() && isTilingFocusRemoved(taskId)) { 719 isTilingFocused = false 720 return false 721 } 722 723 val leftTiledTask = leftTaskResizingHelper ?: return false 724 val rightTiledTask = rightTaskResizingHelper ?: return false 725 if (!allTiledTasksVisible()) return false 726 val isLeftOnTop = taskId == leftTiledTask.taskInfo.taskId 727 if (!isTilingRefocused(taskId)) return false 728 val t = transactionSupplier.get() 729 if (!Flags.enableDisplayFocusInShellTransitions()) isTilingFocused = true 730 if (taskId == leftTaskResizingHelper?.taskInfo?.taskId) { 731 desktopTilingDividerWindowManager?.onRelativeLeashChanged(leftTiledTask.getLeash(), t) 732 } 733 if (taskId == rightTaskResizingHelper?.taskInfo?.taskId) { 734 desktopTilingDividerWindowManager?.onRelativeLeashChanged(rightTiledTask.getLeash(), t) 735 } 736 transitions.startTransition(TRANSIT_TO_FRONT, buildTiledTasksMoveToFront(isLeftOnTop), null) 737 t.apply() 738 return true 739 } 740 getRightSnapBoundsIfTilednull741 fun getRightSnapBoundsIfTiled(): Rect { 742 return getSnapBounds(SnapPosition.RIGHT) 743 } 744 getLeftSnapBoundsIfTilednull745 fun getLeftSnapBoundsIfTiled(): Rect { 746 return getSnapBounds(SnapPosition.LEFT) 747 } 748 allTiledTasksVisiblenull749 private fun allTiledTasksVisible(): Boolean { 750 val leftTiledTask = leftTaskResizingHelper ?: return false 751 val rightTiledTask = rightTaskResizingHelper ?: return false 752 val taskRepository = desktopUserRepositories.current 753 return taskRepository.isVisibleTask(leftTiledTask.taskInfo.taskId) && 754 taskRepository.isVisibleTask(rightTiledTask.taskInfo.taskId) 755 } 756 isResizeWithinSizeConstraintsnull757 private fun isResizeWithinSizeConstraints( 758 newLeftBounds: Rect, 759 newRightBounds: Rect, 760 leftBounds: Rect, 761 rightBounds: Rect, 762 stableBounds: Rect, 763 ): Boolean { 764 return DragPositioningCallbackUtility.isExceedingWidthConstraint( 765 newLeftBounds.width(), 766 leftBounds.width(), 767 stableBounds, 768 displayController, 769 leftTaskResizingHelper?.desktopModeWindowDecoration, 770 ) || 771 DragPositioningCallbackUtility.isExceedingWidthConstraint( 772 newRightBounds.width(), 773 rightBounds.width(), 774 stableBounds, 775 displayController, 776 rightTaskResizingHelper?.desktopModeWindowDecoration, 777 ) 778 } 779 getSnapBoundsnull780 private fun getSnapBounds(position: SnapPosition): Rect { 781 val displayLayout = displayController.getDisplayLayout(displayId) ?: return Rect() 782 783 val stableBounds = Rect() 784 displayLayout.getStableBounds(stableBounds) 785 val leftTiledTask = leftTaskResizingHelper 786 val rightTiledTask = rightTaskResizingHelper 787 val destinationWidth = stableBounds.width() / 2 788 return when (position) { 789 SnapPosition.LEFT -> { 790 val rightBound = 791 if (rightTiledTask == null) { 792 stableBounds.left + destinationWidth - 793 context.resources.getDimensionPixelSize( 794 R.dimen.split_divider_bar_width 795 ) / 2 796 } else { 797 rightTiledTask.bounds.left - 798 context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width) 799 } 800 Rect(stableBounds.left, stableBounds.top, rightBound, stableBounds.bottom) 801 } 802 803 SnapPosition.RIGHT -> { 804 val leftBound = 805 if (leftTiledTask == null) { 806 stableBounds.right - destinationWidth + 807 context.resources.getDimensionPixelSize( 808 R.dimen.split_divider_bar_width 809 ) / 2 810 } else { 811 leftTiledTask.bounds.right + 812 context.resources.getDimensionPixelSize(R.dimen.split_divider_bar_width) 813 } 814 Rect(leftBound, stableBounds.top, stableBounds.right, stableBounds.bottom) 815 } 816 } 817 } 818 inflateDividerBoundsnull819 private fun inflateDividerBounds(displayLayout: DisplayLayout): Rect { 820 val stableBounds = Rect() 821 displayLayout.getStableBounds(stableBounds) 822 823 val leftDividerBounds = leftTaskResizingHelper?.bounds?.right ?: return Rect() 824 val rightDividerBounds = rightTaskResizingHelper?.bounds?.left ?: return Rect() 825 826 // Bounds should never be null here, so assertion is necessary otherwise it's illegal state. 827 return Rect(leftDividerBounds, stableBounds.top, rightDividerBounds, stableBounds.bottom) 828 } 829 tearDownTilingnull830 private fun tearDownTiling() { 831 if (isTilingManagerInitialised) { 832 if (Flags.enableDisplayFocusInShellTransitions()) { 833 focusTransitionObserver.unsetLocalFocusTransitionListener(this) 834 } else { 835 shellTaskOrganizer.removeFocusListener(this) 836 } 837 } 838 839 if (leftTaskResizingHelper == null && rightTaskResizingHelper == null) { 840 shellTaskOrganizer.removeTaskVanishedListener(this) 841 } 842 isTilingFocused = false 843 isTilingManagerInitialised = false 844 desktopTilingDividerWindowManager?.release() 845 desktopTilingDividerWindowManager = null 846 } 847 } 848