1 /* <lambda>null2 * 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.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ValueAnimator 22 import android.annotation.Dimension 23 import android.content.Context 24 import android.graphics.Canvas 25 import android.graphics.Matrix 26 import android.graphics.Paint 27 import android.graphics.Path 28 import android.graphics.Rect 29 import android.graphics.RectF 30 import android.graphics.Region 31 import android.util.AttributeSet 32 import android.view.Display 33 import android.view.DisplayCutout 34 import android.view.DisplayInfo 35 import android.view.Surface 36 import android.view.View 37 import androidx.annotation.VisibleForTesting 38 import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView 39 import com.android.app.animation.Interpolators 40 import com.android.systemui.util.asIndenting 41 import java.io.PrintWriter 42 43 /** 44 * A class that handles common actions of display cutout view. 45 * - Draws cutouts. 46 * - Handles camera protection. 47 * - Intercepts touches on cutout areas. 48 */ 49 open class DisplayCutoutBaseView : View, RegionInterceptableView { 50 51 private var shouldDrawCutout: Boolean = DisplayCutout.getFillBuiltInDisplayCutout( 52 context.resources, context.display?.uniqueId 53 ) 54 private var displayUniqueId: String? = null 55 private var displayMode: Display.Mode? = null 56 protected val location = IntArray(2) 57 protected var displayRotation = 0 58 59 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 60 @JvmField val displayInfo = DisplayInfo() 61 @JvmField protected var pendingConfigChange = false 62 @JvmField protected val paint = Paint() 63 @JvmField protected val cutoutPath = Path() 64 65 @JvmField protected var showProtection = false 66 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 67 @JvmField val protectionRect: RectF = RectF() 68 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 69 @JvmField val protectionPath: Path = Path() 70 private val protectionRectOrig: RectF = RectF() 71 private val protectionPathOrig: Path = Path() 72 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 73 var cameraProtectionProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE 74 private var cameraProtectionAnimator: ValueAnimator? = null 75 76 constructor(context: Context) : super(context) 77 78 constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 79 80 constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : 81 super(context, attrs, defStyleAttr) 82 83 override fun onAttachedToWindow() { 84 super.onAttachedToWindow() 85 displayUniqueId = context.display?.uniqueId 86 updateCutout() 87 updateProtectionBoundingPath() 88 onUpdate() 89 } 90 91 fun updateConfiguration(newDisplayUniqueId: String?) { 92 val info = DisplayInfo() 93 context.display?.getDisplayInfo(info) 94 val oldMode: Display.Mode? = displayMode 95 displayMode = info.mode 96 97 updateDisplayUniqueId(info.uniqueId) 98 99 // Skip if display mode or cutout hasn't changed. 100 if (!displayModeChanged(oldMode, displayMode) && 101 displayInfo.displayCutout == info.displayCutout && 102 displayRotation == info.rotation) { 103 return 104 } 105 if (newDisplayUniqueId == info.uniqueId) { 106 displayRotation = info.rotation 107 updateCutout() 108 updateProtectionBoundingPath() 109 onUpdate() 110 } 111 } 112 113 open fun updateDisplayUniqueId(newDisplayUniqueId: String?) { 114 if (displayUniqueId != newDisplayUniqueId) { 115 displayUniqueId = newDisplayUniqueId 116 shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout( 117 context.resources, displayUniqueId 118 ) 119 invalidate() 120 } 121 } 122 123 open fun updateRotation(rotation: Int) { 124 displayRotation = rotation 125 updateCutout() 126 updateProtectionBoundingPath() 127 onUpdate() 128 } 129 130 // Called after the cutout and protection bounding path change. Subclasses 131 // should make any changes that need to happen based on the change. 132 open fun onUpdate() = Unit 133 134 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 135 public override fun onDraw(canvas: Canvas) { 136 super.onDraw(canvas) 137 if (!shouldDrawCutout) { 138 return 139 } 140 canvas.save() 141 getLocationOnScreen(location) 142 canvas.translate(-location[0].toFloat(), -location[1].toFloat()) 143 144 drawCutouts(canvas) 145 drawCutoutProtection(canvas) 146 canvas.restore() 147 } 148 149 override fun shouldInterceptTouch(): Boolean { 150 return displayInfo.displayCutout != null && visibility == VISIBLE && shouldDrawCutout 151 } 152 153 override fun getInterceptRegion(): Region? { 154 displayInfo.displayCutout ?: return null 155 156 val cutoutBounds: Region = rectsToRegion(displayInfo.displayCutout?.boundingRects) 157 // Transform to window's coordinate space 158 rootView.getLocationOnScreen(location) 159 cutoutBounds.translate(-location[0], -location[1]) 160 161 // Intersect with window's frame 162 cutoutBounds.op( 163 rootView.left, rootView.top, rootView.right, rootView.bottom, Region.Op.INTERSECT 164 ) 165 return cutoutBounds 166 } 167 168 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 169 open fun updateCutout() { 170 if (pendingConfigChange) { 171 return 172 } 173 cutoutPath.reset() 174 context.display?.getDisplayInfo(displayInfo) 175 displayInfo.displayCutout?.cutoutPath?.let { path -> cutoutPath.set(path) } 176 invalidate() 177 } 178 179 @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) 180 open fun drawCutouts(canvas: Canvas) { 181 displayInfo.displayCutout?.cutoutPath ?: return 182 canvas.drawPath(cutoutPath, paint) 183 } 184 185 protected open fun drawCutoutProtection(canvas: Canvas) { 186 if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE && 187 !protectionRect.isEmpty 188 ) { 189 canvas.scale( 190 cameraProtectionProgress, cameraProtectionProgress, protectionRect.centerX(), 191 protectionRect.centerY() 192 ) 193 canvas.drawPath(protectionPath, paint) 194 } 195 } 196 197 /** 198 * Converts a set of [Rect]s into a [Region] 199 */ 200 fun rectsToRegion(rects: List<Rect?>?): Region { 201 val result = Region.obtain() 202 if (rects != null) { 203 for (r in rects) { 204 if (r != null && !r.isEmpty) { 205 result.op(r, Region.Op.UNION) 206 } 207 } 208 } 209 return result 210 } 211 212 open fun enableShowProtection(isCameraActive: Boolean) { 213 if (showProtection == isCameraActive) { 214 return 215 } 216 showProtection = isCameraActive 217 updateProtectionBoundingPath() 218 // Delay the relayout until the end of the animation when hiding the cutout, 219 // otherwise we'd clip it. 220 if (showProtection) { 221 requestLayout() 222 } 223 cameraProtectionAnimator?.cancel() 224 cameraProtectionAnimator = ValueAnimator.ofFloat( 225 cameraProtectionProgress, 226 if (showProtection) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE 227 ).setDuration(750) 228 cameraProtectionAnimator?.interpolator = Interpolators.DECELERATE_QUINT 229 cameraProtectionAnimator?.addUpdateListener( 230 ValueAnimator.AnimatorUpdateListener { animation: ValueAnimator -> 231 cameraProtectionProgress = animation.animatedValue as Float 232 invalidate() 233 } 234 ) 235 cameraProtectionAnimator?.addListener(object : AnimatorListenerAdapter() { 236 override fun onAnimationEnd(animation: Animator) { 237 cameraProtectionAnimator = null 238 if (!showProtection) { 239 requestLayout() 240 } 241 } 242 }) 243 cameraProtectionAnimator?.start() 244 } 245 246 open fun setProtection(path: Path, pathBounds: Rect) { 247 protectionPathOrig.reset() 248 protectionPathOrig.set(path) 249 protectionPath.reset() 250 protectionRectOrig.setEmpty() 251 protectionRectOrig.set(pathBounds) 252 protectionRect.setEmpty() 253 } 254 255 protected open fun updateProtectionBoundingPath() { 256 if (pendingConfigChange) { 257 return 258 } 259 val m = Matrix() 260 // Apply display ratio. 261 val physicalPixelDisplaySizeRatio = getPhysicalPixelDisplaySizeRatio() 262 m.postScale(physicalPixelDisplaySizeRatio, physicalPixelDisplaySizeRatio) 263 264 // Apply rotation. 265 val lw: Int = displayInfo.logicalWidth 266 val lh: Int = displayInfo.logicalHeight 267 val flipped = ( 268 displayInfo.rotation == Surface.ROTATION_90 || 269 displayInfo.rotation == Surface.ROTATION_270 270 ) 271 val dw = if (flipped) lh else lw 272 val dh = if (flipped) lw else lh 273 transformPhysicalToLogicalCoordinates(displayInfo.rotation, dw, dh, m) 274 275 if (!protectionPathOrig.isEmpty) { 276 // Reset the protection path so we don't aggregate rotations 277 protectionPath.set(protectionPathOrig) 278 protectionPath.transform(m) 279 m.mapRect(protectionRect, protectionRectOrig) 280 } 281 } 282 283 @VisibleForTesting 284 open fun getPhysicalPixelDisplaySizeRatio(): Float { 285 displayInfo.displayCutout?.let { 286 return it.cutoutPathParserInfo.physicalPixelDisplaySizeRatio 287 } 288 return 1f 289 } 290 291 private fun displayModeChanged(oldMode: Display.Mode?, newMode: Display.Mode?): Boolean { 292 if (oldMode == null) { 293 return true 294 } 295 296 // We purposely ignore refresh rate and id changes here, because we don't need to 297 // invalidate for those, and they can trigger the refresh rate to increase 298 return oldMode?.physicalHeight != newMode?.physicalHeight || 299 oldMode?.physicalWidth != newMode?.physicalWidth 300 } 301 302 companion object { 303 const val HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f 304 305 @JvmStatic protected fun transformPhysicalToLogicalCoordinates( 306 @Surface.Rotation rotation: Int, 307 @Dimension physicalWidth: Int, 308 @Dimension physicalHeight: Int, 309 out: Matrix 310 ) { 311 when (rotation) { 312 Surface.ROTATION_0 -> return 313 Surface.ROTATION_90 -> { 314 out.postRotate(270f) 315 out.postTranslate(0f, physicalWidth.toFloat()) 316 } 317 Surface.ROTATION_180 -> { 318 out.postRotate(180f) 319 out.postTranslate(physicalWidth.toFloat(), physicalHeight.toFloat()) 320 } 321 Surface.ROTATION_270 -> { 322 out.postRotate(90f) 323 out.postTranslate(physicalHeight.toFloat(), 0f) 324 } 325 else -> throw IllegalArgumentException("Unknown rotation: $rotation") 326 } 327 } 328 } 329 330 open fun dump(pw: PrintWriter) { 331 val ipw = pw.asIndenting() 332 ipw.increaseIndent() 333 ipw.println("DisplayCutoutBaseView:") 334 ipw.increaseIndent() 335 ipw.println("shouldDrawCutout=$shouldDrawCutout") 336 ipw.println("cutout=${displayInfo.displayCutout}") 337 ipw.println("cameraProtectionProgress=$cameraProtectionProgress") 338 ipw.println("protectionRect=$protectionRect") 339 ipw.println("protectionRectOrig=$protectionRectOrig") 340 ipw.decreaseIndent() 341 ipw.decreaseIndent() 342 } 343 } 344