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