From 1698145b122ad3e0d7d9ea8652dc1574075cadd1 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Fri, 21 Feb 2020 14:33:42 -0500 Subject: [PATCH] Add SystemUI support for front-facing camera protection Devices with a DisplayCutout configured may want to add some extra area of turned-off pixels around the cutout in order to keep light from leaking into camera hardware. This CL adds two new config values to sysui to enable the configuration of this cutout protection, and listens for CameraManager events telling us that a relevant camera has turned on. Test: manual Bug: 145095085 Change-Id: Ifce67a593247e3a2151d41800ae46a50478e0b7d (cherry picked from commit 6075dd7f58766902e06941552f27ff5ceacd369f) --- packages/SystemUI/res/values/config.xml | 17 +++ .../systemui/CameraAvailabilityListener.kt | 138 ++++++++++++++++++ .../android/systemui/ScreenDecorations.java | 85 ++++++++++- 3 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 27ffcee2a2dc6..d16082915207d 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -506,4 +506,21 @@ @*android:string/status_bar_headset + + + + + + + + false + diff --git a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt new file mode 100644 index 0000000000000..24fa91b9e8385 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2020 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.content.Context +import android.graphics.Path +import android.graphics.Rect +import android.graphics.RectF +import android.hardware.camera2.CameraManager +import android.util.PathParser +import java.util.concurrent.Executor + +import kotlin.math.roundToInt + +const val TAG = "CameraOpTransitionController" + +/** + * Listens for usage of the Camera and controls the ScreenDecorations transition to show extra + * protection around a display cutout based on config_frontBuiltInDisplayCutoutProtection and + * config_enableDisplayCutoutProtection + */ +class CameraAvailabilityListener( + private val cameraManager: CameraManager, + private val cutoutProtectionPath: Path, + private val targetCameraId: String, + private val executor: Executor +) { + private var cutoutBounds = Rect() + private val listeners = mutableListOf() + private val availabilityCallback: CameraManager.AvailabilityCallback = + object : CameraManager.AvailabilityCallback() { + override fun onCameraAvailable(cameraId: String) { + if (targetCameraId == cameraId) { + notifyCameraInactive() + } + } + + override fun onCameraUnavailable(cameraId: String) { + if (targetCameraId == cameraId) { + notifyCameraActive() + } + } + } + + init { + val computed = RectF() + cutoutProtectionPath.computeBounds(computed, false /* unused */) + cutoutBounds.set( + computed.left.roundToInt(), + computed.top.roundToInt(), + computed.right.roundToInt(), + computed.bottom.roundToInt()) + } + + /** + * Start listening for availability events, and maybe notify listeners + * + * @return true if we started listening + */ + fun startListening() { + registerCameraListener() + } + + fun stop() { + unregisterCameraListener() + } + + fun addTransitionCallback(callback: CameraTransitionCallback) { + listeners.add(callback) + } + + fun removeTransitionCallback(callback: CameraTransitionCallback) { + listeners.remove(callback) + } + + private fun registerCameraListener() { + cameraManager.registerAvailabilityCallback(executor, availabilityCallback) + } + + private fun unregisterCameraListener() { + cameraManager.unregisterAvailabilityCallback(availabilityCallback) + } + + private fun notifyCameraActive() { + listeners.forEach { it.onApplyCameraProtection(cutoutProtectionPath, cutoutBounds) } + } + + private fun notifyCameraInactive() { + listeners.forEach { it.onHideCameraProtection() } + } + + /** + * Callbacks to tell a listener that a relevant camera turned on and off. + */ + interface CameraTransitionCallback { + fun onApplyCameraProtection(protectionPath: Path, bounds: Rect) + fun onHideCameraProtection() + } + + companion object Factory { + fun build(context: Context, executor: Executor): CameraAvailabilityListener { + val manager = context + .getSystemService(Context.CAMERA_SERVICE) as CameraManager + val res = context.resources + val pathString = res.getString(R.string.config_frontBuiltInDisplayCutoutProtection) + val cameraId = res.getString(R.string.config_protectedCameraId) + + return CameraAvailabilityListener( + manager, pathFromString(pathString), cameraId, executor) + } + + private fun pathFromString(pathString: String): Path { + val spec = pathString.trim() + val p: Path + try { + p = PathParser.createPathFromPathData(spec) + } catch (e: Throwable) { + throw IllegalArgumentException("Invalid protection path", e) + } + + return p + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 0f896c44ae636..1324524c4011a 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -29,6 +29,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import android.annotation.Dimension; +import android.annotation.NonNull; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -36,6 +37,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.res.ColorStateList; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; @@ -105,6 +107,7 @@ public class ScreenDecorations extends SystemUI implements Tunable { private final Handler mMainHandler; private final TunerService mTunerService; private DisplayManager.DisplayListener mDisplayListener; + private CameraAvailabilityListener mCameraListener; @VisibleForTesting protected int mRoundedDefault; @@ -122,6 +125,26 @@ public class ScreenDecorations extends SystemUI implements Tunable { private boolean mPendingRotationChange; private Handler mHandler; + private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback = + new CameraAvailabilityListener.CameraTransitionCallback() { + @Override + public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { + // Show the extra protection around the front facing camera if necessary + for (DisplayCutoutView dcv : mCutoutViews) { + dcv.setProtection(protectionPath, bounds); + dcv.setShowProtection(true); + } + } + + @Override + public void onHideCameraProtection() { + // Go back to the regular anti-aliasing + for (DisplayCutoutView dcv : mCutoutViews) { + dcv.setShowProtection(false); + } + } + }; + /** * Converts a set of {@link Rect}s into a {@link Region} * @@ -169,6 +192,8 @@ public class ScreenDecorations extends SystemUI implements Tunable { mDisplayManager = mContext.getSystemService(DisplayManager.class); updateRoundedCornerRadii(); setupDecorations(); + setupCameraListener(); + mDisplayListener = new DisplayManager.DisplayListener() { @Override public void onDisplayAdded(int displayId) { @@ -443,6 +468,16 @@ public class ScreenDecorations extends SystemUI implements Tunable { : pos - rotation; } + private void setupCameraListener() { + Resources res = mContext.getResources(); + boolean enabled = res.getBoolean(R.bool.config_enableDisplayCutoutProtection); + if (enabled) { + mCameraListener = CameraAvailabilityListener.Factory.build(mContext, mHandler::post); + mCameraListener.addTransitionCallback(mCameraTransitionCallback); + mCameraListener.startListening(); + } + } + private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -684,6 +719,13 @@ public class ScreenDecorations extends SystemUI implements Tunable { private final List mBounds = new ArrayList(); private final Rect mBoundingRect = new Rect(); private final Path mBoundingPath = new Path(); + // Don't initialize these because they are cached elsewhere and may not exist + private Rect mProtectionRect; + private Path mProtectionPath; + private Rect mTotalBounds = new Rect(); + // Whether or not to show the cutout protection path + private boolean mShowProtection = false; + private final int[] mLocation = new int[2]; private final ScreenDecorations mDecorations; private int mColor = Color.BLACK; @@ -727,7 +769,13 @@ public class ScreenDecorations extends SystemUI implements Tunable { super.onDraw(canvas); getLocationOnScreen(mLocation); canvas.translate(-mLocation[0], -mLocation[1]); - if (!mBoundingPath.isEmpty()) { + + if (mShowProtection && !mProtectionRect.isEmpty()) { + mPaint.setColor(mColor); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setAntiAlias(true); + canvas.drawPath(mProtectionPath, mPaint); + } else if (!mBoundingPath.isEmpty()) { mPaint.setColor(mColor); mPaint.setStyle(Paint.Style.FILL); mPaint.setAntiAlias(true); @@ -755,6 +803,22 @@ public class ScreenDecorations extends SystemUI implements Tunable { update(); } + void setProtection(Path protectionPath, Rect pathBounds) { + mProtectionPath = protectionPath; + mProtectionRect = pathBounds; + } + + void setShowProtection(boolean shouldShow) { + if (mShowProtection == shouldShow) { + return; + } + + mShowProtection = shouldShow; + updateBoundingPath(); + requestLayout(); + invalidate(); + } + private void update() { if (!isAttachedToWindow() || mDecorations.mPendingRotationChange) { return; @@ -794,6 +858,9 @@ public class ScreenDecorations extends SystemUI implements Tunable { Matrix m = new Matrix(); transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m); mBoundingPath.transform(m); + if (mProtectionPath != null) { + mProtectionPath.transform(m); + } } private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation, @@ -855,9 +922,19 @@ public class ScreenDecorations extends SystemUI implements Tunable { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } - setMeasuredDimension( - resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), - resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)); + + if (mShowProtection) { + // Make sure that our measured height encompases the protection + mTotalBounds.union(mBoundingRect); + mTotalBounds.union(mProtectionRect); + setMeasuredDimension( + resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), + resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)); + } else { + setMeasuredDimension( + resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), + resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)); + } } public static void boundsFromDirection(DisplayCutout displayCutout, int gravity,