1 /*
2  * Copyright (C) 2023 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.decor
18 
19 import android.graphics.Canvas
20 import android.graphics.Color
21 import android.graphics.ColorFilter
22 import android.graphics.Paint
23 import android.graphics.Path
24 import android.graphics.PixelFormat
25 import android.graphics.drawable.Drawable
26 import android.util.Size
27 import java.io.PrintWriter
28 
29 /**
30  * Rounded corner delegate that handles incoming debug commands and can convert them to path
31  * drawables to be shown instead of the system-defined rounded corners.
32  *
33  * These debug corners are expected to supersede the system-defined corners
34  */
35 class DebugRoundedCornerDelegate : RoundedCornerResDelegate {
36     override var hasTop: Boolean = false
37         private set
38     override var topRoundedDrawable: Drawable? = null
39         private set
40     override var topRoundedSize: Size = Size(0, 0)
41         private set
42 
43     override var hasBottom: Boolean = false
44         private set
45     override var bottomRoundedDrawable: Drawable? = null
46         private set
47     override var bottomRoundedSize: Size = Size(0, 0)
48         private set
49 
50     override var physicalPixelDisplaySizeRatio: Float = 1f
51         set(value) {
52             if (field == value) {
53                 return
54             }
55             field = value
56             reloadMeasures()
57         }
58 
59     var color: Int = Color.RED
60         set(value) {
61             if (field == value) {
62                 return
63             }
64 
65             field = value
66             paint.color = field
67         }
68 
69     var paint =
70         Paint().apply {
71             color = Color.RED
72             style = Paint.Style.FILL
73         }
74 
75     override fun updateDisplayUniqueId(newDisplayUniqueId: String?, newReloadToken: Int?) {
76         // nop -- debug corners draw the same on every display
77     }
78 
79     fun applyNewDebugCorners(
80         topCorner: DebugRoundedCornerModel?,
81         bottomCorner: DebugRoundedCornerModel?,
82     ) {
83         topCorner?.let {
84             hasTop = true
85             topRoundedDrawable = it.toPathDrawable(paint)
86             topRoundedSize = it.size()
87         }
88             ?: {
89                 hasTop = false
90                 topRoundedDrawable = null
91                 topRoundedSize = Size(0, 0)
92             }
93 
94         bottomCorner?.let {
95             hasBottom = true
96             bottomRoundedDrawable = it.toPathDrawable(paint)
97             bottomRoundedSize = it.size()
98         }
99             ?: {
100                 hasBottom = false
101                 bottomRoundedDrawable = null
102                 bottomRoundedSize = Size(0, 0)
103             }
104     }
105 
106     /**
107      * Remove accumulated debug state by clearing out the drawables and setting [hasTop] and
108      * [hasBottom] to false.
109      */
110     fun removeDebugState() {
111         hasTop = false
112         topRoundedDrawable = null
113         topRoundedSize = Size(0, 0)
114 
115         hasBottom = false
116         bottomRoundedDrawable = null
117         bottomRoundedSize = Size(0, 0)
118     }
119 
120     /**
121      * Scaling here happens when the display resolution is changed. This logic is exactly the same
122      * as in [RoundedCornerResDelegateImpl]
123      */
124     private fun reloadMeasures() {
125         topRoundedDrawable?.let { topRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight) }
126         bottomRoundedDrawable?.let {
127             bottomRoundedSize = Size(it.intrinsicWidth, it.intrinsicHeight)
128         }
129 
130         if (physicalPixelDisplaySizeRatio != 1f) {
131             if (topRoundedSize.width != 0) {
132                 topRoundedSize =
133                     Size(
134                         (physicalPixelDisplaySizeRatio * topRoundedSize.width + 0.5f).toInt(),
135                         (physicalPixelDisplaySizeRatio * topRoundedSize.height + 0.5f).toInt()
136                     )
137             }
138             if (bottomRoundedSize.width != 0) {
139                 bottomRoundedSize =
140                     Size(
141                         (physicalPixelDisplaySizeRatio * bottomRoundedSize.width + 0.5f).toInt(),
142                         (physicalPixelDisplaySizeRatio * bottomRoundedSize.height + 0.5f).toInt()
143                     )
144             }
145         }
146     }
147 
148     fun dump(pw: PrintWriter) {
149         pw.println("DebugRoundedCornerDelegate state:")
150         pw.println("  hasTop=$hasTop")
151         pw.println("  hasBottom=$hasBottom")
152         pw.println("  topRoundedSize(w,h)=(${topRoundedSize.width},${topRoundedSize.height})")
153         pw.println(
154             "  bottomRoundedSize(w,h)=(${bottomRoundedSize.width},${bottomRoundedSize.height})"
155         )
156         pw.println("  physicalPixelDisplaySizeRatio=$physicalPixelDisplaySizeRatio")
157     }
158 }
159 
160 /** Encapsulates the data coming in from the command line args and turns into a [PathDrawable] */
161 data class DebugRoundedCornerModel(
162     val path: Path,
163     val width: Int,
164     val height: Int,
165     val scaleX: Float,
166     val scaleY: Float,
167 ) {
168     fun size() = Size(width, height)
169 
170     fun toPathDrawable(paint: Paint) =
171         PathDrawable(
172             path,
173             width,
174             height,
175             scaleX,
176             scaleY,
177             paint,
178         )
179 }
180 
181 /**
182  * PathDrawable accepts paths from the command line via [DebugRoundedCornerModel], and renders them
183  * in the canvas provided by the screen decor rounded corner provider
184  */
185 class PathDrawable(
186     val path: Path,
187     val width: Int,
188     val height: Int,
189     val scaleX: Float = 1f,
190     val scaleY: Float = 1f,
191     val paint: Paint,
192 ) : Drawable() {
193     private var cf: ColorFilter? = null
194 
195     override fun draw(canvas: Canvas) {
196         if (scaleX != 1f || scaleY != 1f) {
197             canvas.scale(scaleX, scaleY)
198         }
199         canvas.drawPath(path, paint)
200     }
201 
202     override fun getIntrinsicHeight(): Int = height
203     override fun getIntrinsicWidth(): Int = width
204 
205     override fun getOpacity(): Int = PixelFormat.OPAQUE
206 
207     override fun setAlpha(alpha: Int) {}
208 
209     override fun setColorFilter(colorFilter: ColorFilter?) {
210         cf = colorFilter
211     }
212 }
213