• 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 package com.android.wm.shell.windowdecor
17 
18 import android.graphics.PointF
19 import android.graphics.Rect
20 import android.hardware.display.DisplayTopology
21 import android.os.Handler
22 import android.os.IBinder
23 import android.os.Looper
24 import android.view.Choreographer
25 import android.view.Surface
26 import android.view.SurfaceControl
27 import android.view.WindowManager
28 import android.window.TransitionInfo
29 import android.window.TransitionRequestInfo
30 import android.window.WindowContainerTransaction
31 import com.android.internal.jank.Cuj
32 import com.android.internal.jank.InteractionJankMonitor
33 import com.android.wm.shell.ShellTaskOrganizer
34 import com.android.wm.shell.common.DisplayController
35 import com.android.wm.shell.common.MultiDisplayDragMoveBoundsCalculator
36 import com.android.wm.shell.common.MultiDisplayDragMoveIndicatorController
37 import com.android.wm.shell.shared.annotations.ShellMainThread
38 import com.android.wm.shell.transition.Transitions
39 import java.util.concurrent.TimeUnit
40 
41 /**
42  * A task positioner that also takes into account resizing a
43  * [com.android.wm.shell.windowdecor.ResizeVeil] and dragging move across multiple displays.
44  * - If the drag is resizing the task, we resize the veil instead.
45  * - If the drag is repositioning, we consider multi-display topology if needed, and update in the
46  *   typical manner.
47  */
48 class MultiDisplayVeiledResizeTaskPositioner(
49     private val taskOrganizer: ShellTaskOrganizer,
50     private val desktopWindowDecoration: DesktopModeWindowDecoration,
51     private val displayController: DisplayController,
52     dragEventListener: DragPositioningCallbackUtility.DragEventListener,
53     private val transactionSupplier: () -> SurfaceControl.Transaction,
54     private val transitions: Transitions,
55     private val interactionJankMonitor: InteractionJankMonitor,
56     @ShellMainThread private val handler: Handler,
57     private val multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController,
58 ) : TaskPositioner, Transitions.TransitionHandler, DisplayController.OnDisplaysChangedListener {
59     private val dragEventListeners =
60         mutableListOf<DragPositioningCallbackUtility.DragEventListener>()
61     private val stableBounds = Rect()
62     private val taskBoundsAtDragStart = Rect()
63     private val repositionStartPoint = PointF()
64     private val repositionTaskBounds = Rect()
65     private val isResizing: Boolean
66         get() =
67             (ctrlType and DragPositioningCallback.CTRL_TYPE_TOP) != 0 ||
68                 (ctrlType and DragPositioningCallback.CTRL_TYPE_BOTTOM) != 0 ||
69                 (ctrlType and DragPositioningCallback.CTRL_TYPE_LEFT) != 0 ||
70                 (ctrlType and DragPositioningCallback.CTRL_TYPE_RIGHT) != 0
71 
72     @DragPositioningCallback.CtrlType private var ctrlType = 0
73     private var isResizingOrAnimatingResize = false
74     @Surface.Rotation private var rotation = 0
75     private var startDisplayId = 0
76     private val displayIds = mutableSetOf<Int>()
77 
78     constructor(
79         taskOrganizer: ShellTaskOrganizer,
80         windowDecoration: DesktopModeWindowDecoration,
81         displayController: DisplayController,
82         dragEventListener: DragPositioningCallbackUtility.DragEventListener,
83         transitions: Transitions,
84         interactionJankMonitor: InteractionJankMonitor,
85         @ShellMainThread handler: Handler,
86         multiDisplayDragMoveIndicatorController: MultiDisplayDragMoveIndicatorController,
87     ) : this(
88         taskOrganizer,
89         windowDecoration,
90         displayController,
91         dragEventListener,
<lambda>null92         { SurfaceControl.Transaction() },
93         transitions,
94         interactionJankMonitor,
95         handler,
96         multiDisplayDragMoveIndicatorController,
97     )
98 
99     init {
100         dragEventListeners.add(dragEventListener)
101         displayController.addDisplayWindowListener(this)
102     }
103 
onDragPositioningStartnull104     override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect {
105         this.ctrlType = ctrlType
106         startDisplayId = displayId
107         taskBoundsAtDragStart.set(
108             desktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.bounds
109         )
110         repositionStartPoint[x] = y
111         if (isResizing) {
112             // Capture CUJ for re-sizing window in DW mode.
113             interactionJankMonitor.begin(
114                 createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW)
115             )
116             if (!desktopWindowDecoration.mHasGlobalFocus) {
117                 val wct = WindowContainerTransaction()
118                 wct.reorder(
119                     desktopWindowDecoration.mTaskInfo.token,
120                     /* onTop= */ true,
121                     /* includingParents= */ true,
122                 )
123                 taskOrganizer.applyTransaction(wct)
124             }
125         }
126         for (dragEventListener in dragEventListeners) {
127             dragEventListener.onDragStart(desktopWindowDecoration.mTaskInfo.taskId)
128         }
129         repositionTaskBounds.set(taskBoundsAtDragStart)
130         val rotation =
131             desktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.displayRotation
132         if (stableBounds.isEmpty || this.rotation != rotation) {
133             this.rotation = rotation
134             displayController
135                 .getDisplayLayout(desktopWindowDecoration.mDisplay.displayId)!!
136                 .getStableBounds(stableBounds)
137         }
138         return Rect(repositionTaskBounds)
139     }
140 
onDragPositioningMovenull141     override fun onDragPositioningMove(displayId: Int, x: Float, y: Float): Rect {
142         check(Looper.myLooper() == handler.looper) {
143             "This method must run on the shell main thread."
144         }
145         val delta = DragPositioningCallbackUtility.calculateDelta(x, y, repositionStartPoint)
146         if (
147             isResizing &&
148                 DragPositioningCallbackUtility.changeBounds(
149                     ctrlType,
150                     repositionTaskBounds,
151                     taskBoundsAtDragStart,
152                     stableBounds,
153                     delta,
154                     displayController,
155                     desktopWindowDecoration,
156                 )
157         ) {
158             if (!isResizingOrAnimatingResize) {
159                 for (dragEventListener in dragEventListeners) {
160                     dragEventListener.onDragMove(desktopWindowDecoration.mTaskInfo.taskId)
161                 }
162                 desktopWindowDecoration.showResizeVeil(repositionTaskBounds)
163                 isResizingOrAnimatingResize = true
164             } else {
165                 desktopWindowDecoration.updateResizeVeil(repositionTaskBounds)
166             }
167         } else if (ctrlType == DragPositioningCallback.CTRL_TYPE_UNDEFINED) {
168             // Begin window drag CUJ instrumentation only when drag position moves.
169             interactionJankMonitor.begin(
170                 createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW)
171             )
172 
173             val t = transactionSupplier()
174             val startDisplayLayout = displayController.getDisplayLayout(startDisplayId)
175             val currentDisplayLayout = displayController.getDisplayLayout(displayId)
176 
177             if (startDisplayLayout == null || currentDisplayLayout == null) {
178                 // Fall back to single-display drag behavior if any display layout is unavailable.
179                 DragPositioningCallbackUtility.setPositionOnDrag(
180                     desktopWindowDecoration,
181                     repositionTaskBounds,
182                     taskBoundsAtDragStart,
183                     repositionStartPoint,
184                     t,
185                     x,
186                     y,
187                 )
188             } else {
189                 val boundsDp =
190                     MultiDisplayDragMoveBoundsCalculator.calculateGlobalDpBoundsForDrag(
191                         startDisplayLayout,
192                         repositionStartPoint,
193                         taskBoundsAtDragStart,
194                         currentDisplayLayout,
195                         x,
196                         y,
197                     )
198                 repositionTaskBounds.set(
199                     MultiDisplayDragMoveBoundsCalculator.convertGlobalDpToLocalPxForRect(
200                         boundsDp,
201                         startDisplayLayout,
202                     )
203                 )
204 
205                 multiDisplayDragMoveIndicatorController.onDragMove(
206                     boundsDp,
207                     startDisplayId,
208                     desktopWindowDecoration.mTaskInfo,
209                     displayIds,
210                     transactionSupplier,
211                 )
212 
213                 t.setPosition(
214                     desktopWindowDecoration.leash,
215                     repositionTaskBounds.left.toFloat(),
216                     repositionTaskBounds.top.toFloat(),
217                 )
218             }
219             t.setFrameTimeline(Choreographer.getInstance().vsyncId)
220             t.apply()
221         }
222         return Rect(repositionTaskBounds)
223     }
224 
onDragPositioningEndnull225     override fun onDragPositioningEnd(displayId: Int, x: Float, y: Float): Rect {
226         val delta = DragPositioningCallbackUtility.calculateDelta(x, y, repositionStartPoint)
227         if (isResizing) {
228             if (taskBoundsAtDragStart != repositionTaskBounds) {
229                 DragPositioningCallbackUtility.changeBounds(
230                     ctrlType,
231                     repositionTaskBounds,
232                     taskBoundsAtDragStart,
233                     stableBounds,
234                     delta,
235                     displayController,
236                     desktopWindowDecoration,
237                 )
238                 desktopWindowDecoration.updateResizeVeil(repositionTaskBounds)
239                 val wct = WindowContainerTransaction()
240                 wct.setBounds(desktopWindowDecoration.mTaskInfo.token, repositionTaskBounds)
241                 transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, this)
242             } else {
243                 // If bounds haven't changed, perform necessary veil reset here as startAnimation
244                 // won't be called.
245                 resetVeilIfVisible()
246             }
247             interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW)
248         } else {
249             val startDisplayLayout = displayController.getDisplayLayout(startDisplayId)
250             val currentDisplayLayout = displayController.getDisplayLayout(displayId)
251 
252             if (startDisplayId == displayId
253                 || startDisplayLayout == null || currentDisplayLayout == null) {
254                 // Fall back to single-display drag behavior if:
255                 // 1. The drag destination display is the same as the start display. This prevents
256                 // unnecessary animations caused by minor width/height changes due to DPI scaling.
257                 // 2. Either the starting or current display layout is unavailable.
258                 DragPositioningCallbackUtility.updateTaskBounds(
259                     repositionTaskBounds,
260                     taskBoundsAtDragStart,
261                     repositionStartPoint,
262                     x,
263                     y,
264                 )
265             } else {
266                 val boundsDp =
267                     MultiDisplayDragMoveBoundsCalculator.calculateGlobalDpBoundsForDrag(
268                         startDisplayLayout,
269                         repositionStartPoint,
270                         taskBoundsAtDragStart,
271                         currentDisplayLayout,
272                         x,
273                         y,
274                     )
275                 repositionTaskBounds.set(
276                     MultiDisplayDragMoveBoundsCalculator.convertGlobalDpToLocalPxForRect(
277                         boundsDp,
278                         currentDisplayLayout,
279                     )
280                 )
281             }
282 
283             // Call the MultiDisplayDragMoveIndicatorController to clear any active indicator
284             // surfaces. This is necessary even if the drag ended on the same display, as surfaces
285             // may have been created for other displays during the drag.
286             multiDisplayDragMoveIndicatorController.onDragEnd(
287                 desktopWindowDecoration.mTaskInfo.taskId,
288                 transactionSupplier,
289             )
290 
291             interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW)
292         }
293 
294         ctrlType = DragPositioningCallback.CTRL_TYPE_UNDEFINED
295         taskBoundsAtDragStart.setEmpty()
296         repositionStartPoint[0f] = 0f
297         return Rect(repositionTaskBounds)
298     }
299 
closenull300     override fun close() {
301         displayController.removeDisplayWindowListener(this)
302     }
303 
resetVeilIfVisiblenull304     private fun resetVeilIfVisible() {
305         if (isResizingOrAnimatingResize) {
306             desktopWindowDecoration.hideResizeVeil()
307             isResizingOrAnimatingResize = false
308         }
309     }
310 
createLongTimeoutJankConfigBuildernull311     private fun createLongTimeoutJankConfigBuilder(@Cuj.CujType cujType: Int) =
312         InteractionJankMonitor.Configuration.Builder.withSurface(
313                 cujType,
314                 desktopWindowDecoration.mContext,
315                 desktopWindowDecoration.mTaskSurface,
316                 handler,
317             )
318             .setTimeout(LONG_CUJ_TIMEOUT_MS)
319 
320     override fun startAnimation(
321         transition: IBinder,
322         info: TransitionInfo,
323         startTransaction: SurfaceControl.Transaction,
324         finishTransaction: SurfaceControl.Transaction,
325         finishCallback: Transitions.TransitionFinishCallback,
326     ): Boolean {
327         for (change in info.changes) {
328             val sc = change.leash
329             val endBounds = change.endAbsBounds
330             val endPosition = change.endRelOffset
331             startTransaction
332                 .setWindowCrop(sc, endBounds.width(), endBounds.height())
333                 .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
334             finishTransaction
335                 .setWindowCrop(sc, endBounds.width(), endBounds.height())
336                 .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat())
337         }
338 
339         startTransaction.apply()
340         resetVeilIfVisible()
341         ctrlType = DragPositioningCallback.CTRL_TYPE_UNDEFINED
342         finishCallback.onTransitionFinished(null /* wct */)
343         isResizingOrAnimatingResize = false
344         interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW)
345         return true
346     }
347 
348     /**
349      * We should never reach this as this handler's transitions are only started from shell
350      * explicitly.
351      */
handleRequestnull352     override fun handleRequest(
353         transition: IBinder,
354         request: TransitionRequestInfo,
355     ): WindowContainerTransaction? {
356         return null
357     }
358 
isResizingOrAnimatingnull359     override fun isResizingOrAnimating() = isResizingOrAnimatingResize
360 
361     override fun addDragEventListener(
362         dragEventListener: DragPositioningCallbackUtility.DragEventListener
363     ) {
364         dragEventListeners.add(dragEventListener)
365     }
366 
removeDragEventListenernull367     override fun removeDragEventListener(
368         dragEventListener: DragPositioningCallbackUtility.DragEventListener
369     ) {
370         dragEventListeners.remove(dragEventListener)
371     }
372 
onTopologyChangednull373     override fun onTopologyChanged(topology: DisplayTopology?) {
374         // TODO: b/383069173 - Cancel window drag when topology changes happen during drag.
375 
376         displayIds.clear()
377         if (topology == null) return
378         val displayBounds = topology.getAbsoluteBounds()
379         displayIds.addAll(List(displayBounds.size()) { displayBounds.keyAt(it) })
380     }
381 
382     companion object {
383         // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid
384         // timing out in the middle of a resize or drag action.
385         private val LONG_CUJ_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(/* duration= */ 10L)
386     }
387 }
388