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