| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui |
| |
| import android.animation.Animator |
| import android.animation.AnimatorListenerAdapter |
| import android.animation.ValueAnimator |
| import android.annotation.Dimension |
| import android.content.Context |
| import android.graphics.Canvas |
| import android.graphics.Matrix |
| import android.graphics.Paint |
| import android.graphics.Path |
| import android.graphics.Rect |
| import android.graphics.RectF |
| import android.graphics.Region |
| import android.util.AttributeSet |
| import android.view.Display |
| import android.view.DisplayCutout |
| import android.view.DisplayInfo |
| import android.view.Surface |
| import android.view.View |
| import androidx.annotation.VisibleForTesting |
| import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView |
| import com.android.systemui.animation.Interpolators |
| |
| /** |
| * A class that handles common actions of display cutout view. |
| * - Draws cutouts. |
| * - Handles camera protection. |
| * - Intercepts touches on cutout areas. |
| */ |
| open class DisplayCutoutBaseView : View, RegionInterceptableView { |
| |
| private var shouldDrawCutout: Boolean = DisplayCutout.getFillBuiltInDisplayCutout( |
| context.resources, context.display?.uniqueId) |
| private var displayUniqueId: String? = null |
| private var displayMode: Display.Mode? = null |
| protected val location = IntArray(2) |
| protected var displayRotation = 0 |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| @JvmField val displayInfo = DisplayInfo() |
| @JvmField protected var pendingConfigChange = false |
| @JvmField protected val paint = Paint() |
| @JvmField protected val cutoutPath = Path() |
| |
| @JvmField protected var showProtection = false |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| @JvmField val protectionRect: RectF = RectF() |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| @JvmField val protectionPath: Path = Path() |
| private val protectionRectOrig: RectF = RectF() |
| private val protectionPathOrig: Path = Path() |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| var cameraProtectionProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE |
| private var cameraProtectionAnimator: ValueAnimator? = null |
| |
| constructor(context: Context) : super(context) |
| |
| constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) |
| |
| constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) |
| : super(context, attrs, defStyleAttr) |
| |
| override fun onAttachedToWindow() { |
| super.onAttachedToWindow() |
| displayUniqueId = context.display?.uniqueId |
| updateCutout() |
| updateProtectionBoundingPath() |
| onUpdate() |
| } |
| |
| fun onDisplayChanged(displayId: Int) { |
| val oldMode: Display.Mode? = displayMode |
| val display: Display? = context.display |
| displayMode = display?.mode |
| |
| if (displayUniqueId != display?.uniqueId) { |
| displayUniqueId = display?.uniqueId |
| shouldDrawCutout = DisplayCutout.getFillBuiltInDisplayCutout( |
| context.resources, displayUniqueId) |
| } |
| |
| // Skip if display mode or cutout hasn't changed. |
| if (!displayModeChanged(oldMode, displayMode) && |
| display?.cutout == displayInfo.displayCutout) { |
| return |
| } |
| if (displayId == display?.displayId) { |
| updateCutout() |
| updateProtectionBoundingPath() |
| onUpdate() |
| } |
| } |
| |
| open fun updateRotation(rotation: Int) { |
| displayRotation = rotation |
| updateCutout() |
| updateProtectionBoundingPath() |
| onUpdate() |
| } |
| |
| // Called after the cutout and protection bounding path change. Subclasses |
| // should make any changes that need to happen based on the change. |
| open fun onUpdate() = Unit |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| public override fun onDraw(canvas: Canvas) { |
| super.onDraw(canvas) |
| if (!shouldDrawCutout) { |
| return |
| } |
| canvas.save() |
| getLocationOnScreen(location) |
| canvas.translate(-location[0].toFloat(), -location[1].toFloat()) |
| |
| drawCutouts(canvas) |
| drawCutoutProtection(canvas) |
| canvas.restore() |
| } |
| |
| override fun shouldInterceptTouch(): Boolean { |
| return displayInfo.displayCutout != null && visibility == VISIBLE && shouldDrawCutout |
| } |
| |
| override fun getInterceptRegion(): Region? { |
| displayInfo.displayCutout ?: return null |
| |
| val cutoutBounds: Region = rectsToRegion(displayInfo.displayCutout?.boundingRects) |
| // Transform to window's coordinate space |
| rootView.getLocationOnScreen(location) |
| cutoutBounds.translate(-location[0], -location[1]) |
| |
| // Intersect with window's frame |
| cutoutBounds.op(rootView.left, rootView.top, rootView.right, rootView.bottom, |
| Region.Op.INTERSECT) |
| return cutoutBounds |
| } |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| open fun updateCutout() { |
| if (pendingConfigChange) { |
| return |
| } |
| cutoutPath.reset() |
| display.getDisplayInfo(displayInfo) |
| displayInfo.displayCutout?.cutoutPath?.let { path -> cutoutPath.set(path) } |
| invalidate() |
| } |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) |
| open fun drawCutouts(canvas: Canvas) { |
| displayInfo.displayCutout?.cutoutPath ?: return |
| canvas.drawPath(cutoutPath, paint) |
| } |
| |
| protected open fun drawCutoutProtection(canvas: Canvas) { |
| if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE && |
| !protectionRect.isEmpty) { |
| canvas.scale(cameraProtectionProgress, cameraProtectionProgress, |
| protectionRect.centerX(), protectionRect.centerY()) |
| canvas.drawPath(protectionPath, paint) |
| } |
| } |
| |
| /** |
| * Converts a set of [Rect]s into a [Region] |
| */ |
| fun rectsToRegion(rects: List<Rect?>?): Region { |
| val result = Region.obtain() |
| if (rects != null) { |
| for (r in rects) { |
| if (r != null && !r.isEmpty) { |
| result.op(r, Region.Op.UNION) |
| } |
| } |
| } |
| return result |
| } |
| |
| open fun enableShowProtection(show: Boolean) { |
| if (showProtection == show) { |
| return |
| } |
| showProtection = show |
| updateProtectionBoundingPath() |
| // Delay the relayout until the end of the animation when hiding the cutout, |
| // otherwise we'd clip it. |
| if (showProtection) { |
| requestLayout() |
| } |
| cameraProtectionAnimator?.cancel() |
| cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress, |
| if (showProtection) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750) |
| cameraProtectionAnimator?.interpolator = Interpolators.DECELERATE_QUINT |
| cameraProtectionAnimator?.addUpdateListener(ValueAnimator.AnimatorUpdateListener { |
| animation: ValueAnimator -> |
| cameraProtectionProgress = animation.animatedValue as Float |
| invalidate() |
| }) |
| cameraProtectionAnimator?.addListener(object : AnimatorListenerAdapter() { |
| override fun onAnimationEnd(animation: Animator) { |
| cameraProtectionAnimator = null |
| if (!showProtection) { |
| requestLayout() |
| } |
| } |
| }) |
| cameraProtectionAnimator?.start() |
| } |
| |
| open fun setProtection(path: Path, pathBounds: Rect) { |
| protectionPathOrig.reset() |
| protectionPathOrig.set(path) |
| protectionPath.reset() |
| protectionRectOrig.setEmpty() |
| protectionRectOrig.set(pathBounds) |
| protectionRect.setEmpty() |
| } |
| |
| protected open fun updateProtectionBoundingPath() { |
| if (pendingConfigChange) { |
| return |
| } |
| val m = Matrix() |
| // Apply display ratio. |
| val physicalPixelDisplaySizeRatio = getPhysicalPixelDisplaySizeRatio() |
| m.postScale(physicalPixelDisplaySizeRatio, physicalPixelDisplaySizeRatio) |
| |
| // Apply rotation. |
| val lw: Int = displayInfo.logicalWidth |
| val lh: Int = displayInfo.logicalHeight |
| val flipped = (displayInfo.rotation == Surface.ROTATION_90 || |
| displayInfo.rotation == Surface.ROTATION_270) |
| val dw = if (flipped) lh else lw |
| val dh = if (flipped) lw else lh |
| transformPhysicalToLogicalCoordinates(displayInfo.rotation, dw, dh, m) |
| |
| if (!protectionPathOrig.isEmpty) { |
| // Reset the protection path so we don't aggregate rotations |
| protectionPath.set(protectionPathOrig) |
| protectionPath.transform(m) |
| m.mapRect(protectionRect, protectionRectOrig) |
| } |
| } |
| |
| @VisibleForTesting |
| open fun getPhysicalPixelDisplaySizeRatio(): Float { |
| displayInfo.displayCutout?.let { |
| return it.cutoutPathParserInfo.physicalPixelDisplaySizeRatio |
| } |
| return 1f |
| } |
| |
| private fun displayModeChanged(oldMode: Display.Mode?, newMode: Display.Mode?): Boolean { |
| if (oldMode == null) { |
| return true |
| } |
| |
| // We purposely ignore refresh rate and id changes here, because we don't need to |
| // invalidate for those, and they can trigger the refresh rate to increase |
| return oldMode?.physicalHeight != newMode?.physicalHeight || |
| oldMode?.physicalWidth != newMode?.physicalWidth |
| } |
| |
| companion object { |
| const val HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f |
| |
| @JvmStatic protected fun transformPhysicalToLogicalCoordinates( |
| @Surface.Rotation rotation: Int, |
| @Dimension physicalWidth: Int, |
| @Dimension physicalHeight: Int, |
| out: Matrix |
| ) { |
| when (rotation) { |
| Surface.ROTATION_0 -> return |
| Surface.ROTATION_90 -> { |
| out.postRotate(270f) |
| out.postTranslate(0f, physicalWidth.toFloat()) |
| } |
| Surface.ROTATION_180 -> { |
| out.postRotate(180f) |
| out.postTranslate(physicalWidth.toFloat(), physicalHeight.toFloat()) |
| } |
| Surface.ROTATION_270 -> { |
| out.postRotate(90f) |
| out.postTranslate(physicalHeight.toFloat(), 0f) |
| } |
| else -> throw IllegalArgumentException("Unknown rotation: $rotation") |
| } |
| } |
| } |
| } |