• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.content.res.Configuration
24 import android.graphics.Path
25 import android.graphics.PixelFormat
26 import android.graphics.Rect
27 import android.graphics.Region
28 import android.os.Binder
29 import android.util.Size
30 import android.view.LayoutInflater
31 import android.view.MotionEvent
32 import android.view.RoundedCorner
33 import android.view.SurfaceControl
34 import android.view.SurfaceControlViewHost
35 import android.view.View
36 import android.view.WindowManager
37 import android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
38 import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
39 import android.view.WindowManager.LayoutParams.FLAG_SLIPPERY
40 import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
41 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
42 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
43 import android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER
44 import android.view.WindowlessWindowManager
45 import com.android.wm.shell.R
46 import com.android.wm.shell.common.SyncTransactionQueue
47 import java.util.function.Supplier
48 
49 /**
50  * a [WindowlessWindowManaer] responsible for hosting the [TilingDividerView] on the display root
51  * when two tasks are tiled on left and right to resize them simultaneously.
52  */
53 class DesktopTilingDividerWindowManager(
54     config: Configuration,
55     private val windowName: String,
56     private val context: Context,
57     private val leash: SurfaceControl,
58     private val syncQueue: SyncTransactionQueue,
59     private val transitionHandler: DesktopTilingWindowDecoration,
60     private val transactionSupplier: Supplier<SurfaceControl.Transaction>,
61     private var dividerBounds: Rect,
62     private val displayContext: Context,
63     private val isDarkMode: Boolean,
64 ) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener {
65     private lateinit var viewHost: SurfaceControlViewHost
66     private var tilingDividerView: TilingDividerView? = null
67     private var dividerShown = false
68     private var handleRegionSize: Size =
69         Size(
70             context.resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_width),
71             context.resources.getDimensionPixelSize(R.dimen.split_divider_handle_region_height),
72         )
73     private var setTouchRegion = true
74     private val maxRoundedCornerRadius = getMaxRoundedCornerRadius()
75 
76     /**
77      * Gets bounds of divider window with screen based coordinate on the param Rect.
78      *
79      * @param rect bounds for the [TilingDividerView]
80      */
81     fun getDividerBounds(rect: Rect) {
82         rect.set(dividerBounds)
83     }
84 
85     /**
86      * Sets the touch region for the SurfaceControlViewHost.
87      *
88      * The region includes the area around the handle (for accessibility), the divider itself and
89      * the rounded corners (to prevent click reaching windows behind).
90      */
91     fun setTouchRegion(handle: Rect, divider: Rect, cornerRadius: Float) {
92         val path = Path()
93         path.fillType = Path.FillType.WINDING
94         // The UI starts on the top-left corner, the region will be:
95         //
96         //      cornerLeft     cornerRight
97         // c1Top        +--------+
98         //              |corners |
99         // c1Bottom     +--+  +--+
100         //                 |  |
101         //       handleLeft|  |  handleRight
102         // handleTop  +----+  +----+
103         //            |  handle    |
104         // handleBot  +----+  +----+
105         //                 |  |
106         //                 |  |
107         // c2Top        +--+  +--+
108         //              |corners |
109         // c2Bottom     +--------+
110         val cornerLeft = 0f
111         val centerX = cornerRadius + divider.width() / 2f
112         val centerY = divider.height()
113         val cornerRight = divider.width() + 2 * cornerRadius
114         val handleLeft = centerX - handle.width() / 2f
115         val handleRight = handleLeft + handle.width()
116         val dividerLeft = centerY - divider.width() / 2f
117         val dividerRight = dividerLeft + divider.width()
118 
119         val c1Top = 0f
120         val c1Bottom = cornerRadius
121         val handleTop = centerY - handle.height() / 2f
122         val handleBottom = handleTop + handle.height()
123         val c2Top = divider.height() - cornerRadius
124         val c2Bottom = divider.height().toFloat()
125 
126         // Top corners
127         path.addRect(cornerLeft, c1Top, cornerRight, c1Bottom, Path.Direction.CCW)
128         // Bottom corners
129         path.addRect(cornerLeft, c1Top, cornerRight, c2Bottom, Path.Direction.CCW)
130         // Handle
131         path.addRect(handleLeft, handleTop, handleRight, handleBottom, Path.Direction.CCW)
132         // Divider
133         path.addRect(dividerLeft, c2Top, dividerRight, c2Bottom, Path.Direction.CCW)
134 
135         val clip = Rect(handleLeft.toInt(), c1Top.toInt(), handleRight.toInt(), c2Bottom.toInt())
136 
137         val region = Region()
138         region.setPath(path, Region(clip))
139 
140         setTouchRegion(viewHost.windowToken.asBinder(), region)
141     }
142 
143     /**
144      * Builds a view host upon tiling two tasks left and right, and shows the divider view in the
145      * middle of the screen between both tasks.
146      *
147      * @param relativeLeash the task leash that the TilingDividerView should be shown on top of.
148      */
149     fun generateViewHost(relativeLeash: SurfaceControl) {
150         val surfaceControlViewHost =
151             SurfaceControlViewHost(context, context.display, this, "DesktopTilingManager")
152         val dividerView =
153             LayoutInflater.from(context).inflate(R.layout.tiling_split_divider, /* root= */ null)
154                 as TilingDividerView
155         val lp = getWindowManagerParams()
156         surfaceControlViewHost.setView(dividerView, lp)
157         val tmpDividerBounds = Rect()
158         getDividerBounds(tmpDividerBounds)
159         dividerView.setup(this, tmpDividerBounds, handleRegionSize, isDarkMode)
160         val dividerAnimatorT = transactionSupplier.get()
161         val dividerAnimator =
162             ValueAnimator.ofFloat(0f, 1f).apply {
163                 duration = DIVIDER_FADE_IN_ALPHA_DURATION
164                 addUpdateListener {
165                     dividerAnimatorT.setAlpha(leash, animatedValue as Float).apply()
166                 }
167                 addListener(
168                     object : AnimatorListenerAdapter() {
169                         override fun onAnimationStart(animation: Animator) {
170                             dividerAnimatorT
171                                 .setRelativeLayer(leash, relativeLeash, 1)
172                                 .setPosition(
173                                     leash,
174                                     dividerBounds.left.toFloat() - maxRoundedCornerRadius,
175                                     dividerBounds.top.toFloat(),
176                                 )
177                                 .setAlpha(leash, 0f)
178                                 .show(leash)
179                                 .apply()
180                         }
181 
182                         override fun onAnimationEnd(animation: Animator) {
183                             dividerAnimatorT.setAlpha(leash, 1f).apply()
184                             dividerShown = true
185                         }
186                     }
187                 )
188             }
189         dividerAnimator.start()
190         viewHost = surfaceControlViewHost
191         tilingDividerView = dividerView
192         updateTouchRegion()
193         dividerView.addOnLayoutChangeListener(this)
194     }
195 
196     /** Changes divider colour if dark/light mode is toggled. */
197     fun onUiModeChange(isDarkMode: Boolean) {
198         tilingDividerView?.onUiModeChange(isDarkMode)
199     }
200 
201     /** Notifies the divider view of task info change and possible color change. */
202     fun onTaskInfoChange() {
203         tilingDividerView?.onTaskInfoChange()
204     }
205 
206     /** Hides the divider bar. */
207     fun hideDividerBar() {
208         if (!dividerShown) {
209             return
210         }
211         val t = transactionSupplier.get()
212         t.hide(leash)
213         t.apply()
214         dividerShown = false
215     }
216 
217     /** Shows the divider bar. */
218     fun showDividerBar() {
219         if (dividerShown) return
220         val t = transactionSupplier.get()
221         t.show(leash)
222         t.apply()
223         dividerShown = true
224     }
225 
226     /**
227      * When the tiled task on top changes, the divider bar's Z access should change to be on top of
228      * the latest focused task.
229      */
230     fun onRelativeLeashChanged(relativeLeash: SurfaceControl, t: SurfaceControl.Transaction) {
231         t.setRelativeLayer(leash, relativeLeash, 1)
232     }
233 
234     override fun onDividerMoveStart(pos: Int, motionEvent: MotionEvent) {
235         setSlippery(false)
236         transitionHandler.onDividerHandleDragStart(motionEvent)
237     }
238 
239     /**
240      * Moves the divider view to a new position after touch, gets called from the
241      * [TilingDividerView] onTouch function.
242      */
243     override fun onDividerMove(pos: Int): Boolean {
244         val t = transactionSupplier.get()
245         t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat())
246         val dividerWidth = dividerBounds.width()
247         dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
248         return transitionHandler.onDividerHandleMoved(dividerBounds, t)
249     }
250 
251     /**
252      * Notifies the transition handler of tiling operations ending, which might result in resizing
253      * WindowContainerTransactions if the sizes of the tiled tasks changed.
254      */
255     override fun onDividerMovedEnd(pos: Int, motionEvent: MotionEvent) {
256         setSlippery(true)
257         val t = transactionSupplier.get()
258         t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat())
259         val dividerWidth = dividerBounds.width()
260         dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom)
261         transitionHandler.onDividerHandleDragEnd(dividerBounds, t, motionEvent)
262     }
263 
264     private fun getWindowManagerParams(): WindowManager.LayoutParams {
265         val lp =
266             WindowManager.LayoutParams(
267                 /* w= */ dividerBounds.width() + 2 * maxRoundedCornerRadius,
268                 /* h= */ dividerBounds.height(),
269                 TYPE_DOCK_DIVIDER,
270                 FLAG_NOT_FOCUSABLE or
271                     FLAG_NOT_TOUCH_MODAL or
272                     FLAG_WATCH_OUTSIDE_TOUCH or
273                     FLAG_SLIPPERY,
274                 PixelFormat.TRANSLUCENT,
275             )
276         lp.token = Binder()
277         lp.title = windowName
278         lp.privateFlags =
279             lp.privateFlags or (PRIVATE_FLAG_NO_MOVE_ANIMATION or PRIVATE_FLAG_TRUSTED_OVERLAY)
280         return lp
281     }
282 
283     /**
284      * Releases the surface control of the current [TilingDividerView] and tear down the view
285      * hierarchy.y.
286      */
287     fun release() {
288         tilingDividerView = null
289         viewHost.release()
290         transactionSupplier.get().hide(leash).remove(leash).apply()
291     }
292 
293     override fun onLayoutChange(
294         v: View?,
295         left: Int,
296         top: Int,
297         right: Int,
298         bottom: Int,
299         oldLeft: Int,
300         oldTop: Int,
301         oldRight: Int,
302         oldBottom: Int,
303     ) {
304         if (!setTouchRegion) return
305 
306         updateTouchRegion()
307         setTouchRegion = false
308     }
309 
310     private fun updateTouchRegion() {
311         val startX = -handleRegionSize.width / 2
312         val handle = Rect(startX, 0, startX + handleRegionSize.width, dividerBounds.height())
313         setTouchRegion(handle, dividerBounds, maxRoundedCornerRadius.toFloat())
314     }
315 
316     private fun setSlippery(slippery: Boolean) {
317         val lp = tilingDividerView?.layoutParams as WindowManager.LayoutParams
318         val isSlippery = (lp.flags and FLAG_SLIPPERY) != 0
319         if (isSlippery == slippery) return
320 
321         if (slippery) {
322             lp.flags = lp.flags or FLAG_SLIPPERY
323         } else {
324             lp.flags = lp.flags and FLAG_SLIPPERY.inv()
325         }
326         viewHost.relayout(lp)
327     }
328 
329     private fun getMaxRoundedCornerRadius(): Int {
330         val display = displayContext.display
331         return listOf(
332                 RoundedCorner.POSITION_TOP_LEFT,
333                 RoundedCorner.POSITION_TOP_RIGHT,
334                 RoundedCorner.POSITION_BOTTOM_RIGHT,
335                 RoundedCorner.POSITION_BOTTOM_LEFT,
336             )
337             .maxOf { position -> display.getRoundedCorner(position)?.getRadius() ?: 0 }
338     }
339 
340     companion object {
341         private const val DIVIDER_FADE_IN_ALPHA_DURATION = 300L
342     }
343 }
344