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