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