Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1698145b authored by Evan Laird's avatar Evan Laird
Browse files

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 6075dd7f)
parent 019ff80d
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -506,4 +506,21 @@
        <item>@*android:string/status_bar_headset</item>
    </string-array>

    <!-- A path similar to frameworks/base/core/res/res/values/config.xml
      config_mainBuiltInDisplayCutout that describes a path larger than the exact path of a display
      cutout. If present as well as config_enableDisplayCutoutProtection is set to true, then
      SystemUI will draw this "protection path" instead of the display cutout path that is normally
      used for anti-aliasing.

      This path will only be drawn when the front-facing camera turns on, otherwise the main
      DisplayCutout path will be rendered
       -->
    <string translatable="false" name="config_frontBuiltInDisplayCutoutProtection"></string>

    <!--  ID for the camera that needs extra protection -->
    <string translatable="false" name="config_protectedCameraId"></string>

    <!--  Flag to turn on the rendering of the above path or not  -->
    <bool name="config_enableDisplayCutoutProtection">false</bool>

</resources>
+138 −0
Original line number Diff line number Diff line
/*
 * 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<CameraTransitionCallback>()
    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
+81 −4
Original line number Diff line number Diff line
@@ -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<Rect> 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,10 +922,20 @@ public class ScreenDecorations extends SystemUI implements Tunable {
                super.onMeasure(widthMeasureSpec, heightMeasureSpec);
                return;
            }

            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,
                Rect out) {