1 /*
2  * Copyright (C) 2022 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.systemui
18 
19 import android.content.Context
20 import android.content.pm.ActivityInfo
21 import android.graphics.Canvas
22 import android.graphics.Color
23 import android.graphics.ColorFilter
24 import android.graphics.Paint
25 import android.graphics.PixelFormat
26 import android.graphics.PorterDuff
27 import android.graphics.PorterDuffColorFilter
28 import android.graphics.PorterDuffXfermode
29 import android.graphics.Rect
30 import android.graphics.Region
31 import android.graphics.drawable.Drawable
32 import android.hardware.graphics.common.AlphaInterpretation
33 import android.hardware.graphics.common.DisplayDecorationSupport
34 import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM
35 import android.view.DisplayCutout.BOUNDS_POSITION_LEFT
36 import android.view.DisplayCutout.BOUNDS_POSITION_LENGTH
37 import android.view.DisplayCutout.BOUNDS_POSITION_TOP
38 import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT
39 import android.view.RoundedCorner
40 import android.view.RoundedCorners
41 import android.view.Surface
42 import androidx.annotation.VisibleForTesting
43 import com.android.systemui.util.asIndenting
44 import java.io.PrintWriter
45 import kotlin.math.ceil
46 import kotlin.math.floor
47 
48 /**
49  * When the HWC of the device supports Composition.DISPLAY_DECORATION, we use this layer to draw
50  * screen decorations.
51  */
52 class ScreenDecorHwcLayer(
53     context: Context,
54     displayDecorationSupport: DisplayDecorationSupport,
55     private val debug: Boolean,
56 ) : DisplayCutoutBaseView(context) {
57     val colorMode: Int
58     private val useInvertedAlphaColor: Boolean
59     private var color: Int = Color.BLACK
60         set(value) {
61             field = value
62             paint.color = value
63         }
64 
65     private val bgColor: Int
66     private var cornerFilter: ColorFilter
67     private val cornerBgFilter: ColorFilter
68     private val clearPaint: Paint
69     @JvmField val transparentRect: Rect = Rect()
70     private val debugTransparentRegionPaint: Paint?
71     private val tempRect: Rect = Rect()
72 
73     private var hasTopRoundedCorner = false
74     private var hasBottomRoundedCorner = false
75     private var roundedCornerTopSize = 0
76     private var roundedCornerBottomSize = 0
77     private var roundedCornerDrawableTop: Drawable? = null
78     private var roundedCornerDrawableBottom: Drawable? = null
79 
80     init {
81         if (displayDecorationSupport.format != PixelFormat.R_8) {
82             throw IllegalArgumentException("Attempting to use unsupported mode " +
83                     "${PixelFormat.formatToString(displayDecorationSupport.format)}")
84         }
85         if (debug) {
86             color = Color.GREEN
87             bgColor = Color.TRANSPARENT
88             colorMode = ActivityInfo.COLOR_MODE_DEFAULT
89             useInvertedAlphaColor = false
90             debugTransparentRegionPaint = Paint().apply {
91                 color = 0x2f00ff00 // semi-transparent green
92                 style = Paint.Style.FILL
93             }
94         } else {
95             colorMode = ActivityInfo.COLOR_MODE_A8
96             useInvertedAlphaColor = displayDecorationSupport.alphaInterpretation ==
97                     AlphaInterpretation.COVERAGE
98             if (useInvertedAlphaColor) {
99                 color = Color.TRANSPARENT
100                 bgColor = Color.BLACK
101             } else {
102                 color = Color.BLACK
103                 bgColor = Color.TRANSPARENT
104             }
105             debugTransparentRegionPaint = null
106         }
107         cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
108         cornerBgFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_OUT)
109 
110         clearPaint = Paint()
111         clearPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
112     }
113 
114     override fun onAttachedToWindow() {
115         super.onAttachedToWindow()
116         parent.requestTransparentRegion(this)
117         updateColors()
118     }
119 
120     private fun updateColors() {
121         if (!debug) {
122             viewRootImpl.setDisplayDecoration(true)
123         }
124 
125         cornerFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
126 
127         if (useInvertedAlphaColor) {
128             paint.set(clearPaint)
129         } else {
130             paint.color = color
131             paint.style = Paint.Style.FILL
132         }
133     }
134 
135     fun setDebugColor(color: Int) {
136         if (!debug) {
137             return
138         }
139 
140         if (this.color == color) {
141             return
142         }
143 
144         this.color = color
145 
146         updateColors()
147         invalidate()
148     }
149 
150     override fun onUpdate() {
151         parent.requestTransparentRegion(this)
152     }
153 
154     override fun onDraw(canvas: Canvas) {
155         // If updating onDraw, also update gatherTransparentRegion
156         if (useInvertedAlphaColor) {
157             canvas.drawColor(bgColor)
158         }
159 
160         // We may clear the color(if useInvertedAlphaColor is true) of the rounded corner rects
161         // before drawing rounded corners. If the cutout happens to be inside one of these rects, it
162         // will be cleared, so we have to draw rounded corners before cutout.
163         drawRoundedCorners(canvas)
164         // Cutouts are drawn in DisplayCutoutBaseView.onDraw()
165         super.onDraw(canvas)
166 
167         debugTransparentRegionPaint?.let {
168             canvas.drawRect(transparentRect, it)
169         }
170     }
171 
172     override fun gatherTransparentRegion(region: Region?): Boolean {
173         region?.let {
174             calculateTransparentRect()
175             if (debug) {
176                 // Since we're going to draw a rectangle where the layer would
177                 // normally be transparent, treat the transparent region as
178                 // empty. We still want this method to be called, though, so
179                 // that it calculates the transparent rect at the right time
180                 // to match ![debug]
181                 region.setEmpty()
182             } else {
183                 region.op(transparentRect, Region.Op.INTERSECT)
184             }
185         }
186         // Always return false - views underneath this should always be visible.
187         return false
188     }
189 
190     /**
191      * The transparent rect is calculated by subtracting the regions of cutouts, cutout protect and
192      * rounded corners from the region with fullscreen display size.
193      */
194     @VisibleForTesting
195     fun calculateTransparentRect() {
196         transparentRect.set(0, 0, width, height)
197 
198         // Remove cutout region.
199         removeCutoutFromTransparentRegion()
200 
201         // Remove cutout protection region.
202         removeCutoutProtectionFromTransparentRegion()
203 
204         // Remove rounded corner region.
205         removeRoundedCornersFromTransparentRegion()
206     }
207 
208     private fun removeCutoutFromTransparentRegion() {
209         displayInfo.displayCutout?.let {
210                 cutout ->
211             if (!cutout.boundingRectLeft.isEmpty) {
212                 transparentRect.left =
213                     cutout.boundingRectLeft.right.coerceAtLeast(transparentRect.left)
214             }
215             if (!cutout.boundingRectTop.isEmpty) {
216                 transparentRect.top =
217                     cutout.boundingRectTop.bottom.coerceAtLeast(transparentRect.top)
218             }
219             if (!cutout.boundingRectRight.isEmpty) {
220                 transparentRect.right =
221                     cutout.boundingRectRight.left.coerceAtMost(transparentRect.right)
222             }
223             if (!cutout.boundingRectBottom.isEmpty) {
224                 transparentRect.bottom =
225                     cutout.boundingRectBottom.top.coerceAtMost(transparentRect.bottom)
226             }
227         }
228     }
229 
230     private fun removeCutoutProtectionFromTransparentRegion() {
231         if (protectionRect.isEmpty) {
232             return
233         }
234 
235         val centerX = protectionRect.centerX()
236         val centerY = protectionRect.centerY()
237         val scaledDistanceX = (centerX - protectionRect.left) * cameraProtectionProgress
238         val scaledDistanceY = (centerY - protectionRect.top) * cameraProtectionProgress
239         tempRect.set(
240             floor(centerX - scaledDistanceX).toInt(),
241             floor(centerY - scaledDistanceY).toInt(),
242             ceil(centerX + scaledDistanceX).toInt(),
243             ceil(centerY + scaledDistanceY).toInt()
244         )
245 
246         // Find out which edge the protectionRect belongs and remove that edge from the transparent
247         // region.
248         val leftDistance = tempRect.left
249         val topDistance = tempRect.top
250         val rightDistance = width - tempRect.right
251         val bottomDistance = height - tempRect.bottom
252         val minDistance = minOf(leftDistance, topDistance, rightDistance, bottomDistance)
253         when (minDistance) {
254             leftDistance -> {
255                 transparentRect.left = tempRect.right.coerceAtLeast(transparentRect.left)
256             }
257             topDistance -> {
258                 transparentRect.top = tempRect.bottom.coerceAtLeast(transparentRect.top)
259             }
260             rightDistance -> {
261                 transparentRect.right = tempRect.left.coerceAtMost(transparentRect.right)
262             }
263             bottomDistance -> {
264                 transparentRect.bottom = tempRect.top.coerceAtMost(transparentRect.bottom)
265             }
266         }
267     }
268 
269     private fun removeRoundedCornersFromTransparentRegion() {
270         var hasTopOrBottomCutouts = false
271         var hasLeftOrRightCutouts = false
272         displayInfo.displayCutout?.let {
273                 cutout ->
274             hasTopOrBottomCutouts = !cutout.boundingRectTop.isEmpty ||
275                     !cutout.boundingRectBottom.isEmpty
276             hasLeftOrRightCutouts = !cutout.boundingRectLeft.isEmpty ||
277                     !cutout.boundingRectRight.isEmpty
278         }
279         // The goal is to remove the rounded corner areas as small as possible so that we can have a
280         // larger transparent region. Therefore, we should always remove from the short edge sides
281         // if possible.
282         val isShortEdgeTopBottom = width < height
283         if (isShortEdgeTopBottom) {
284             // Short edges on top & bottom.
285             if (!hasTopOrBottomCutouts && hasLeftOrRightCutouts) {
286                 // If there are cutouts only on left or right edges, remove left and right sides
287                 // for rounded corners.
288                 transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT)
289                     .coerceAtLeast(transparentRect.left)
290                 transparentRect.right =
291                     (width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT))
292                         .coerceAtMost(transparentRect.right)
293             } else {
294                 // If there are cutouts on top or bottom edges or no cutout at all, remove top
295                 // and bottom sides for rounded corners.
296                 transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP)
297                     .coerceAtLeast(transparentRect.top)
298                 transparentRect.bottom =
299                     (height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM))
300                         .coerceAtMost(transparentRect.bottom)
301             }
302         } else {
303             // Short edges on left & right.
304             if (hasTopOrBottomCutouts && !hasLeftOrRightCutouts) {
305                 // If there are cutouts only on top or bottom edges, remove top and bottom sides
306                 // for rounded corners.
307                 transparentRect.top = getRoundedCornerSizeByPosition(BOUNDS_POSITION_TOP)
308                     .coerceAtLeast(transparentRect.top)
309                 transparentRect.bottom =
310                     (height - getRoundedCornerSizeByPosition(BOUNDS_POSITION_BOTTOM))
311                         .coerceAtMost(transparentRect.bottom)
312             } else {
313                 // If there are cutouts on left or right edges or no cutout at all, remove left
314                 // and right sides for rounded corners.
315                 transparentRect.left = getRoundedCornerSizeByPosition(BOUNDS_POSITION_LEFT)
316                     .coerceAtLeast(transparentRect.left)
317                 transparentRect.right =
318                     (width - getRoundedCornerSizeByPosition(BOUNDS_POSITION_RIGHT))
319                         .coerceAtMost(transparentRect.right)
320             }
321         }
322     }
323 
324     private fun getRoundedCornerSizeByPosition(position: Int): Int {
325         val delta = displayRotation - Surface.ROTATION_0
326         return when ((position + delta) % BOUNDS_POSITION_LENGTH) {
327             BOUNDS_POSITION_LEFT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize)
328             BOUNDS_POSITION_TOP -> roundedCornerTopSize
329             BOUNDS_POSITION_RIGHT -> roundedCornerTopSize.coerceAtLeast(roundedCornerBottomSize)
330             BOUNDS_POSITION_BOTTOM -> roundedCornerBottomSize
331             else -> throw IllegalArgumentException("Incorrect position: $position")
332         }
333     }
334 
335     private fun drawRoundedCorners(canvas: Canvas) {
336         if (!hasTopRoundedCorner && !hasBottomRoundedCorner) {
337             return
338         }
339         var degree: Int
340         for (i in RoundedCorner.POSITION_TOP_LEFT
341                 until RoundedCorners.ROUNDED_CORNER_POSITION_LENGTH) {
342             canvas.save()
343             degree = getRoundedCornerRotationDegree(90 * i)
344             canvas.rotate(degree.toFloat())
345             canvas.translate(
346                     getRoundedCornerTranslationX(degree).toFloat(),
347                     getRoundedCornerTranslationY(degree).toFloat())
348             if (hasTopRoundedCorner && (i == RoundedCorner.POSITION_TOP_LEFT ||
349                             i == RoundedCorner.POSITION_TOP_RIGHT)) {
350                 drawRoundedCorner(canvas, roundedCornerDrawableTop, roundedCornerTopSize)
351             } else if (hasBottomRoundedCorner && (i == RoundedCorner.POSITION_BOTTOM_LEFT ||
352                             i == RoundedCorner.POSITION_BOTTOM_RIGHT)) {
353                 drawRoundedCorner(canvas, roundedCornerDrawableBottom, roundedCornerBottomSize)
354             }
355             canvas.restore()
356         }
357     }
358 
359     private fun drawRoundedCorner(canvas: Canvas, drawable: Drawable?, size: Int) {
360         if (useInvertedAlphaColor) {
361             canvas.drawRect(0f, 0f, size.toFloat(), size.toFloat(), clearPaint)
362             drawable?.colorFilter = cornerBgFilter
363         } else {
364             drawable?.colorFilter = cornerFilter
365         }
366         drawable?.draw(canvas)
367         // Clear color filter when we are done with drawing.
368         drawable?.clearColorFilter()
369     }
370 
371     private fun getRoundedCornerRotationDegree(defaultDegree: Int): Int {
372         return (defaultDegree - 90 * displayRotation + 360) % 360
373     }
374 
375     private fun getRoundedCornerTranslationX(degree: Int): Int {
376         return when (degree) {
377             0, 90 -> 0
378             180 -> -width
379             270 -> -height
380             else -> throw IllegalArgumentException("Incorrect degree: $degree")
381         }
382     }
383 
384     private fun getRoundedCornerTranslationY(degree: Int): Int {
385         return when (degree) {
386             0, 270 -> 0
387             90 -> -width
388             180 -> -height
389             else -> throw IllegalArgumentException("Incorrect degree: $degree")
390         }
391     }
392 
393     /**
394      * Update the rounded corner drawables.
395      */
396     fun updateRoundedCornerDrawable(top: Drawable?, bottom: Drawable?) {
397         roundedCornerDrawableTop = top
398         roundedCornerDrawableBottom = bottom
399         updateRoundedCornerDrawableBounds()
400         invalidate()
401     }
402 
403     /**
404      * Update the rounded corner existence and size.
405      */
406     fun updateRoundedCornerExistenceAndSize(
407         hasTop: Boolean,
408         hasBottom: Boolean,
409         topSize: Int,
410         bottomSize: Int
411     ) {
412         if (hasTopRoundedCorner == hasTop &&
413                 hasBottomRoundedCorner == hasBottom &&
414                 roundedCornerTopSize == topSize &&
415                 roundedCornerBottomSize == bottomSize) {
416             return
417         }
418         hasTopRoundedCorner = hasTop
419         hasBottomRoundedCorner = hasBottom
420         roundedCornerTopSize = topSize
421         roundedCornerBottomSize = bottomSize
422         updateRoundedCornerDrawableBounds()
423 
424         // Use requestLayout() to trigger transparent region recalculated
425         requestLayout()
426     }
427 
428     private fun updateRoundedCornerDrawableBounds() {
429         if (roundedCornerDrawableTop != null) {
430             roundedCornerDrawableTop?.setBounds(0, 0, roundedCornerTopSize,
431                     roundedCornerTopSize)
432         }
433         if (roundedCornerDrawableBottom != null) {
434             roundedCornerDrawableBottom?.setBounds(0, 0, roundedCornerBottomSize,
435                     roundedCornerBottomSize)
436         }
437         invalidate()
438     }
439 
440     override fun dump(pw: PrintWriter) {
441         val ipw = pw.asIndenting()
442         ipw.increaseIndent()
443         ipw.println("ScreenDecorHwcLayer:")
444         super.dump(pw)
445         ipw.println("this=$this")
446         ipw.println("transparentRect=$transparentRect")
447         ipw.println("hasTopRoundedCorner=$hasTopRoundedCorner")
448         ipw.println("hasBottomRoundedCorner=$hasBottomRoundedCorner")
449         ipw.println("roundedCornerTopSize=$roundedCornerTopSize")
450         ipw.println("roundedCornerBottomSize=$roundedCornerBottomSize")
451         ipw.decreaseIndent()
452     }
453 }
454