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