From c2be0bcad74707aa2214213d53b0f29650f275a3 Mon Sep 17 00:00:00 2001 From: Beverly Date: Fri, 13 May 2022 18:45:40 +0000 Subject: [PATCH] Add Face Scanning anim Render the face scanning animation and the camera protection in FaceScanningOverlay, a SW layer. Implementation notes: * The face scanning animation cannot be rendered in the HW layer because it requires color. * We cannot render the face scanning and camera protection overlays in different layers (HW and SW), because then they are difficult to coordinate. => Therefore, we render both the camera + face scanning animation in a SW layer together. Test: atest ScreenDecorationsTest Test: debug mode (adb shell setprop debug.screenshot_rounded_corners 1) Test: enable color inversion, see camera protection is still black Test: enable/disable face scanning feature and see that camera protection and face scanning anim works. Also test privacy dot indicator view still shows appropriately with and w/o the face scanning animation Test: Check systemui dumpsys for view visibilities. Face scanning overlay should NOT be visible unless playing its animation. Bug: 185128224 Change-Id: I2f67257bfcbfe177c76ebe76f20b1d1553e87175 --- .../SystemUI/res-keyguard/values/config.xml | 5 + packages/SystemUI/res/values/ids.xml | 3 + .../keyguard/KeyguardUpdateMonitor.java | 10 +- .../android/systemui/DisplayCutoutBaseView.kt | 2 +- .../android/systemui/FaceScanningOverlay.kt | 260 +++++++++++++++++ .../android/systemui/ScreenDecorHwcLayer.kt | 2 +- .../android/systemui/ScreenDecorations.java | 265 ++++++++++++------ .../android/systemui/decor/DecorProvider.kt | 14 +- .../decor/FaceScanningProviderFactory.kt | 181 ++++++++++++ .../decor/PrivacyDotDecorProviderFactory.kt | 4 + .../src/com/android/systemui/flags/Flags.java | 3 + .../events/PrivacyDotViewController.kt | 2 +- .../systemui/ScreenDecorationsTest.java | 203 +++++++++++--- 13 files changed, 813 insertions(+), 141 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt create mode 100644 packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml index a25ab5109fa80..b1d33758f1b3d 100644 --- a/packages/SystemUI/res-keyguard/values/config.xml +++ b/packages/SystemUI/res-keyguard/values/config.xml @@ -28,6 +28,11 @@ false + + true 3500 diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 5eacc3e6006b3..dca5ea836fa47 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -176,5 +176,8 @@ + + + diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 1ffadb47c0c92..727d108df339d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -1040,7 +1040,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private void handleFaceError(int msgId, String errString) { Assert.isMainThread(); - if (DEBUG_FACE) Log.d(TAG, "Face error received: " + errString); + if (DEBUG_FACE) Log.d(TAG, "Face error received: " + errString + " msgId=" + msgId); if (mHandler.hasCallbacks(mFaceCancelNotReceived)) { mHandler.removeCallbacks(mFaceCancelNotReceived); } @@ -2183,6 +2183,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && mBiometricEnabledForUser.get(userId)); } + public boolean isFaceSupported() { + return mFaceManager != null && mFaceManager.isHardwareDetected(); + } + /** * @return true if there's at least one udfps enrolled for the current user. */ @@ -2309,6 +2313,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab stopListeningForFace(); } + public boolean isFaceScanning() { + return mFaceRunningState == BIOMETRIC_STATE_RUNNING; + } + private void updateFaceListeningState(int action) { // If this message exists, we should not authenticate again until this message is // consumed by the handler diff --git a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt index 34164f3b7306a..d2c229b8ead11 100644 --- a/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt +++ b/packages/SystemUI/src/com/android/systemui/DisplayCutoutBaseView.kt @@ -279,7 +279,7 @@ open class DisplayCutoutBaseView : View, RegionInterceptableView { } companion object { - private const val HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f + const val HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f @JvmStatic protected fun transformPhysicalToLogicalCoordinates( @Surface.Rotation rotation: Int, diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt new file mode 100644 index 0000000000000..b0530f825b9c1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -0,0 +1,260 @@ +/* + * 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.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Path +import android.graphics.RectF +import android.view.View +import androidx.core.graphics.ColorUtils +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.settingslib.Utils +import com.android.systemui.animation.Interpolators +import com.android.systemui.plugins.statusbar.StatusBarStateController + +/** + * When the face is enrolled, we use this view to show the face scanning animation and the camera + * protection on the keyguard. + */ +class FaceScanningOverlay( + context: Context, + pos: Int, + val statusBarStateController: StatusBarStateController, + val keyguardUpdateMonitor: KeyguardUpdateMonitor +) : ScreenDecorations.DisplayCutoutView(context, pos) { + private var showScanningAnim = false + private val rimPaint = Paint() + private var rimProgress: Float = HIDDEN_CAMERA_PROTECTION_SCALE + private var rimAnimator: AnimatorSet? = null + private val rimRect = RectF() + private var cameraProtectionColor = Color.BLACK + var faceScanningAnimColor = Utils.getColorAttrDefaultColor(context, + com.android.systemui.R.attr.wallpaperTextColorAccent) + + override fun setColor(color: Int) { + cameraProtectionColor = color + invalidate() + } + + private var cameraProtectionAnimator: ValueAnimator? = null + var hideOverlayRunnable: Runnable? = null + + override fun drawCutoutProtection(canvas: Canvas) { + if (rimProgress > HIDDEN_RIM_SCALE && !protectionRect.isEmpty) { + val rimPath = Path(protectionPath) + val scaleMatrix = Matrix().apply { + val rimBounds = RectF() + rimPath.computeBounds(rimBounds, true) + setScale(rimProgress, rimProgress, rimBounds.centerX(), rimBounds.centerY()) + } + rimPath.transform(scaleMatrix) + rimPaint.style = Paint.Style.FILL + val rimPaintAlpha = rimPaint.alpha + rimPaint.color = ColorUtils.blendARGB( + faceScanningAnimColor, + Color.WHITE, + statusBarStateController.dozeAmount) + rimPaint.alpha = rimPaintAlpha + canvas.drawPath(rimPath, rimPaint) + } + + if (cameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE && + !protectionRect.isEmpty) { + val scaledProtectionPath = Path(protectionPath) + val scaleMatrix = Matrix().apply { + val protectionPathRect = RectF() + scaledProtectionPath.computeBounds(protectionPathRect, true) + setScale(cameraProtectionProgress, cameraProtectionProgress, + protectionPathRect.centerX(), protectionPathRect.centerY()) + } + scaledProtectionPath.transform(scaleMatrix) + paint.style = Paint.Style.FILL + paint.color = cameraProtectionColor + canvas.drawPath(scaledProtectionPath, paint) + } + } + + override fun enableShowProtection(show: Boolean) { + val showScanningAnimNow = keyguardUpdateMonitor.isFaceScanning && show + if (showScanningAnimNow == showScanningAnim) { + return + } + + showScanningAnim = showScanningAnimNow + updateProtectionBoundingPath() + // Delay the relayout until the end of the animation when hiding, + // otherwise we'd clip it. + if (showScanningAnim) { + visibility = View.VISIBLE + requestLayout() + } + + cameraProtectionAnimator?.cancel() + cameraProtectionAnimator = ValueAnimator.ofFloat(cameraProtectionProgress, + if (showScanningAnimNow) 1.0f else HIDDEN_CAMERA_PROTECTION_SCALE).apply { + startDelay = if (showScanningAnim) 0 else PULSE_DISAPPEAR_DURATION + duration = if (showScanningAnim) PULSE_APPEAR_DURATION else + CAMERA_PROTECTION_DISAPPEAR_DURATION + interpolator = if (showScanningAnim) Interpolators.STANDARD else + Interpolators.EMPHASIZED + + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + cameraProtectionProgress = animation.animatedValue as Float + invalidate() + }) + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + cameraProtectionAnimator = null + if (!showScanningAnim) { + visibility = View.INVISIBLE + hideOverlayRunnable?.run() + hideOverlayRunnable = null + requestLayout() + } + } + }) + start() + } + + rimAnimator?.cancel() + rimAnimator = AnimatorSet().apply { + val rimAppearOrDisappearAnimator = ValueAnimator.ofFloat(rimProgress, + if (showScanningAnim) PULSE_RADIUS_OUT else (PULSE_RADIUS_IN * 1.15f)).apply { + duration = if (showScanningAnim) PULSE_APPEAR_DURATION else PULSE_DISAPPEAR_DURATION + interpolator = Interpolators.STANDARD + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimProgress = animation.animatedValue as Float + invalidate() + }) + } + if (showScanningAnim) { + // appear and then pulse in/out + playSequentially(rimAppearOrDisappearAnimator, + createPulseAnimator(), createPulseAnimator(), + createPulseAnimator(), createPulseAnimator(), + createPulseAnimator(), createPulseAnimator()) + } else { + val opacityAnimator = ValueAnimator.ofInt(255, 0).apply { + duration = PULSE_DISAPPEAR_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimPaint.alpha = animation.animatedValue as Int + invalidate() + }) + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimProgress = HIDDEN_RIM_SCALE + rimPaint.alpha = 255 + invalidate() + } + }) + + // disappear + playTogether(rimAppearOrDisappearAnimator, opacityAnimator) + } + + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + rimAnimator = null + if (!showScanningAnim) { + requestLayout() + } + } + }) + start() + } + } + + fun createPulseAnimator(): AnimatorSet { + return AnimatorSet().apply { + val pulseInwards = ValueAnimator.ofFloat( + PULSE_RADIUS_OUT, PULSE_RADIUS_IN).apply { + duration = PULSE_DURATION_INWARDS + interpolator = Interpolators.STANDARD + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimProgress = animation.animatedValue as Float + invalidate() + }) + } + val pulseOutwards = ValueAnimator.ofFloat( + PULSE_RADIUS_IN, PULSE_RADIUS_OUT).apply { + duration = PULSE_DURATION_OUTWARDS + interpolator = Interpolators.STANDARD + addUpdateListener(ValueAnimator.AnimatorUpdateListener { + animation: ValueAnimator -> + rimProgress = animation.animatedValue as Float + invalidate() + }) + } + playSequentially(pulseInwards, pulseOutwards) + } + } + + override fun updateProtectionBoundingPath() { + super.updateProtectionBoundingPath() + rimRect.set(protectionRect) + rimRect.scale(rimProgress) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + if (mBounds.isEmpty()) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + return + } + if (showScanningAnim) { + // Make sure that our measured height encompasses the extra space for the animation + mTotalBounds.union(mBoundingRect) + mTotalBounds.union( + rimRect.left.toInt(), + rimRect.top.toInt(), + rimRect.right.toInt(), + rimRect.bottom.toInt()) + setMeasuredDimension( + resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), + resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)) + } else { + setMeasuredDimension( + resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), + resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)) + } + } + + companion object { + private const val HIDDEN_RIM_SCALE = HIDDEN_CAMERA_PROTECTION_SCALE + + private const val PULSE_APPEAR_DURATION = 350L + private const val PULSE_DURATION_INWARDS = 500L + private const val PULSE_DURATION_OUTWARDS = 500L + private const val PULSE_DISAPPEAR_DURATION = 850L + private const val CAMERA_PROTECTION_DISAPPEAR_DURATION = 700L // excluding start delay + private const val PULSE_RADIUS_IN = 1.15f + private const val PULSE_RADIUS_OUT = 1.25f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt index 0df2730a48ebb..a74f2f8a5e34e 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorHwcLayer.kt @@ -44,7 +44,7 @@ import kotlin.math.ceil import kotlin.math.floor /** - * When the HWC of the device supports Composition.DISPLAY_DECORATON, we use this layer to draw + * When the HWC of the device supports Composition.DISPLAY_DECORATION, we use this layer to draw * screen decorations. */ class ScreenDecorHwcLayer(context: Context, displayDecorationSupport: DisplayDecorationSupport) diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index dc1a1e0097433..e09c16cafe768 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -69,12 +69,14 @@ import android.widget.FrameLayout; import androidx.annotation.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.settingslib.Utils; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.decor.DecorProvider; import com.android.systemui.decor.DecorProviderFactory; import com.android.systemui.decor.DecorProviderKt; +import com.android.systemui.decor.FaceScanningProviderFactory; import com.android.systemui.decor.OverlayWindow; import com.android.systemui.decor.PrivacyDotDecorProviderFactory; import com.android.systemui.decor.RoundedCornerDecorProviderFactory; @@ -90,8 +92,10 @@ import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.concurrent.Executor; import javax.inject.Inject; @@ -131,6 +135,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab private final PrivacyDotViewController mDotViewController; private final ThreadFactory mThreadFactory; private final DecorProviderFactory mDotFactory; + private final FaceScanningProviderFactory mFaceScanningFactory; + public final int mFaceScanningViewId; @VisibleForTesting protected RoundedCornerResDelegate mRoundedCornerResDelegate; @@ -161,46 +167,84 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @VisibleForTesting protected DisplayInfo mDisplayInfo = new DisplayInfo(); + @VisibleForTesting + protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { + if (mFaceScanningFactory.shouldShowFaceScanningAnim()) { + DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView( + mFaceScanningViewId); + if (overlay != null) { + overlay.setProtection(protectionPath, bounds); + overlay.enableShowProtection(true); + updateOverlayWindowVisibilityIfViewExists( + overlay.findViewById(mFaceScanningViewId)); + // immediately return, bc FaceScanningOverlay also renders the camera + // protection, so we don't need to show the camera protection in + // mScreenDecorHwcLayer or mCutoutViews + return; + } + } + + if (mScreenDecorHwcLayer != null) { + mScreenDecorHwcLayer.setProtection(protectionPath, bounds); + mScreenDecorHwcLayer.enableShowProtection(true); + return; + } + + if (mCutoutViews == null) { + Log.w(TAG, "DisplayCutoutView not initialized onApplyCameraProtection"); + return; + } + + // Show the extra protection around the front facing camera if necessary + for (DisplayCutoutView dcv : mCutoutViews) { + // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile + if (dcv != null) { + dcv.setProtection(protectionPath, bounds); + dcv.enableShowProtection(true); + } + } + } + + @VisibleForTesting + protected void hideCameraProtection() { + FaceScanningOverlay faceScanningOverlay = + (FaceScanningOverlay) getOverlayView(mFaceScanningViewId); + if (faceScanningOverlay != null) { + faceScanningOverlay.setHideOverlayRunnable(() -> { + updateOverlayWindowVisibilityIfViewExists( + faceScanningOverlay.findViewById(mFaceScanningViewId)); + }); + faceScanningOverlay.enableShowProtection(false); + } + + if (mScreenDecorHwcLayer != null) { + mScreenDecorHwcLayer.enableShowProtection(false); + return; + } + + if (mCutoutViews == null) { + Log.w(TAG, "DisplayCutoutView not initialized onHideCameraProtection"); + return; + } + // Go back to the regular anti-aliasing + for (DisplayCutoutView dcv : mCutoutViews) { + // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile + if (dcv != null) { + dcv.enableShowProtection(false); + } + } + } + private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback = new CameraAvailabilityListener.CameraTransitionCallback() { @Override public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { - if (mScreenDecorHwcLayer != null) { - mScreenDecorHwcLayer.setProtection(protectionPath, bounds); - mScreenDecorHwcLayer.enableShowProtection(true); - return; - } - if (mCutoutViews == null) { - Log.w(TAG, "DisplayCutoutView do not initialized"); - return; - } - // Show the extra protection around the front facing camera if necessary - for (DisplayCutoutView dcv : mCutoutViews) { - // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile - if (dcv != null) { - dcv.setProtection(protectionPath, bounds); - dcv.enableShowProtection(true); - } - } + showCameraProtection(protectionPath, bounds); } @Override public void onHideCameraProtection() { - if (mScreenDecorHwcLayer != null) { - mScreenDecorHwcLayer.enableShowProtection(false); - return; - } - if (mCutoutViews == null) { - Log.w(TAG, "DisplayCutoutView do not initialized"); - return; - } - // Go back to the regular anti-aliasing - for (DisplayCutoutView dcv : mCutoutViews) { - // Check Null since not all mCutoutViews[pos] be inflated at the meanwhile - if (dcv != null) { - dcv.enableShowProtection(false); - } - } + hideCameraProtection(); } }; @@ -209,25 +253,24 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab new PrivacyDotViewController.ShowingListener() { @Override public void onPrivacyDotShown(@Nullable View v) { - setOverlayWindowVisibilityIfViewExist(v, View.VISIBLE); + updateOverlayWindowVisibilityIfViewExists(v); } @Override public void onPrivacyDotHidden(@Nullable View v) { - setOverlayWindowVisibilityIfViewExist(v, View.INVISIBLE); + updateOverlayWindowVisibilityIfViewExists(v); } }; @VisibleForTesting - protected void setOverlayWindowVisibilityIfViewExist(@Nullable View view, - @View.Visibility int visibility) { + protected void updateOverlayWindowVisibilityIfViewExists(@Nullable View view) { if (view == null) { return; } mExecutor.execute(() -> { // We don't need to control the window visibility if rounded corners or cutout is drawn // on sw layer since the overlay windows are always visible in this case. - if (mOverlays == null || !isOnlyPrivacyDotInSwLayer()) { + if (mOverlays == null || !shouldOptimizeVisibility()) { return; } @@ -236,7 +279,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab continue; } if (overlay.getView(view.getId()) != null) { - overlay.getRootView().setVisibility(visibility); + overlay.getRootView().setVisibility(getWindowVisibility(overlay, true)); return; } } @@ -258,7 +301,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab UserTracker userTracker, PrivacyDotViewController dotViewController, ThreadFactory threadFactory, - PrivacyDotDecorProviderFactory dotFactory) { + PrivacyDotDecorProviderFactory dotFactory, + FaceScanningProviderFactory faceScanningFactory) { super(context); mMainExecutor = mainExecutor; mSecureSettings = secureSettings; @@ -268,6 +312,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab mDotViewController = dotViewController; mThreadFactory = threadFactory; mDotFactory = dotFactory; + mFaceScanningFactory = faceScanningFactory; + mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim; } @Override @@ -289,6 +335,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @NonNull private List getProviders(boolean hasHwLayer) { List decorProviders = new ArrayList<>(mDotFactory.getProviders()); + decorProviders.addAll(mFaceScanningFactory.getProviders()); if (!hasHwLayer) { decorProviders.addAll(mRoundedCornerFactory.getProviders()); } @@ -466,7 +513,6 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab if (overlay == null) { continue; } - final View view = overlay.getView(id); if (view != null) { return view; @@ -503,7 +549,9 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } private void setupDecorations() { - if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled()) { + if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled() + || mFaceScanningFactory.getHasProviders()) { + List decorProviders = getProviders(mHwcScreenDecorationSupport != null); removeRedundantOverlayViews(decorProviders); @@ -512,21 +560,24 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } else { removeHwcOverlay(); } + final DisplayCutout cutout = getCutout(); - final boolean isOnlyPrivacyDotInSwLayer = isOnlyPrivacyDotInSwLayer(); + final boolean shouldOptimizeVisibility = shouldOptimizeVisibility(); for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) { - if (shouldShowSwLayerCutout(i, cutout) || shouldShowSwLayerRoundedCorner(i, cutout) + if (shouldShowSwLayerCutout(i, cutout) + || shouldShowSwLayerFaceScan(i, cutout) + || shouldShowSwLayerRoundedCorner(i, cutout) || shouldShowSwLayerPrivacyDot(i, cutout)) { Pair, List> pair = DecorProviderKt.partitionAlignedBound(decorProviders, i); decorProviders = pair.getSecond(); - createOverlay(i, pair.getFirst(), isOnlyPrivacyDotInSwLayer); + createOverlay(i, pair.getFirst(), shouldOptimizeVisibility); } else { removeOverlay(i); } } - if (isOnlyPrivacyDotInSwLayer) { + if (shouldOptimizeVisibility) { mDotViewController.setShowingListener(mPrivacyDotShowingListener); } else { mDotViewController.setShowingListener(null); @@ -565,6 +616,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } mColorInversionSetting.setListening(true); mColorInversionSetting.onChange(false); + updateColorInversion(mColorInversionSetting.getValue()); IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_SWITCHED); @@ -626,59 +678,61 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab @View.Visibility private int getWindowVisibility(@NonNull OverlayWindow overlay, - boolean isOnlyPrivacyDotInSwLayer) { - if (!isOnlyPrivacyDotInSwLayer) { - // Multiple views inside overlay, no need to optimize + boolean shouldOptimizeVisibility) { + if (!shouldOptimizeVisibility) { + // All overlays have visible views so there's no need to optimize visibility. + // For example, the rounded corners could exist in each overlay and since the rounded + // corners are always visible, there's no need to optimize visibility. return View.VISIBLE; } + // Optimize if it's just the privacy dot & face scanning animation, since the privacy + // dot and face scanning overlay aren't always visible. int[] ids = { R.id.privacy_dot_top_left_container, R.id.privacy_dot_top_right_container, R.id.privacy_dot_bottom_left_container, - R.id.privacy_dot_bottom_right_container + R.id.privacy_dot_bottom_right_container, + mFaceScanningViewId }; for (int id: ids) { - final View view = overlay.getView(id); - if (view != null && view.getVisibility() == View.VISIBLE) { - // Only privacy dot in sw layers, overlay shall be VISIBLE if one of privacy dot - // views inside this overlay is VISIBLE + final View notAlwaysVisibleViews = overlay.getView(id); + if (notAlwaysVisibleViews != null + && notAlwaysVisibleViews.getVisibility() == View.VISIBLE) { + // Overlay is VISIBLE if one the views inside this overlay is VISIBLE return View.VISIBLE; } } - // Only privacy dot in sw layers, overlay shall be INVISIBLE like default if no privacy dot - // view inside this overlay is VISIBLE. + + // Only non-visible views in this overlay, so set overlay to INVISIBLE return View.INVISIBLE; } private void createOverlay( @BoundsPosition int pos, @NonNull List decorProviders, - boolean isOnlyPrivacyDotInSwLayer) { + boolean shouldOptimizeVisibility) { if (mOverlays == null) { mOverlays = new OverlayWindow[BOUNDS_POSITION_LENGTH]; } if (mOverlays[pos] != null) { - initOverlay(mOverlays[pos], decorProviders, isOnlyPrivacyDotInSwLayer); + initOverlay(mOverlays[pos], decorProviders, shouldOptimizeVisibility); return; } - mOverlays[pos] = new OverlayWindow(mContext); - initOverlay(mOverlays[pos], decorProviders, isOnlyPrivacyDotInSwLayer); + initOverlay(mOverlays[pos], decorProviders, shouldOptimizeVisibility); final ViewGroup overlayView = mOverlays[pos].getRootView(); overlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); overlayView.setAlpha(0); overlayView.setForceDarkAllowed(false); - // Only show cutout and rounded corners in mOverlays when hwc don't support screen - // decoration. + // Only show cutout in mOverlays when hwc doesn't support screen decoration if (mHwcScreenDecorationSupport == null) { if (mCutoutViews == null) { mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH]; } mCutoutViews[pos] = new DisplayCutoutView(mContext, pos); - mCutoutViews[pos].setColor(mTintColor); overlayView.addView(mCutoutViews[pos]); mCutoutViews[pos].updateRotation(mRotation); } @@ -736,7 +790,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab private void initOverlay( @NonNull OverlayWindow overlay, @NonNull List decorProviders, - boolean isOnlyPrivacyDotInSwLayer) { + boolean shouldOptimizeVisibility) { if (!overlay.hasSameProviders(decorProviders)) { decorProviders.forEach(provider -> { if (overlay.getView(provider.getViewId()) != null) { @@ -746,9 +800,10 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab overlay.addDecorProvider(provider, mRotation); }); } - // Use visibility of privacy dot views if only privacy dot in sw layer - overlay.getRootView().setVisibility( - getWindowVisibility(overlay, isOnlyPrivacyDotInSwLayer)); + // Use visibility of privacy dot views & face scanning view to determine the overlay's + // visibility if the screen decoration SW layer overlay isn't persistenly showing + // (ie: rounded corners always showing in SW layer) + overlay.getRootView().setVisibility(getWindowVisibility(overlay, shouldOptimizeVisibility)); } @VisibleForTesting @@ -888,23 +943,28 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab mTintColor = Color.RED; } - // When the hwc supports screen decorations, the layer will use the A8 color mode which - // won't be affected by the color inversion. If the composition goes the client composition - // route, the color inversion will be handled by the RenderEngine. - if (mOverlays == null || mHwcScreenDecorationSupport != null) { + if (mOverlays == null) { return; } - ColorStateList tintList = ColorStateList.valueOf(mTintColor); - mRoundedCornerResDelegate.setColorTintList(tintList); - - Integer[] roundedCornerIds = { - R.id.rounded_corner_top_left, - R.id.rounded_corner_top_right, - R.id.rounded_corner_bottom_left, - R.id.rounded_corner_bottom_right - }; - + // When the hwc supports screen decorations, the layer will use the A8 color mode which + // won't be affected by the color inversion. If the composition goes the client composition + // route, the color inversion will be handled by the RenderEngine. + final Set viewsMayNeedColorUpdate = new HashSet<>(); + if (mHwcScreenDecorationSupport == null) { + ColorStateList tintList = ColorStateList.valueOf(mTintColor); + mRoundedCornerResDelegate.setColorTintList(tintList); + viewsMayNeedColorUpdate.add(R.id.rounded_corner_top_left); + viewsMayNeedColorUpdate.add(R.id.rounded_corner_top_right); + viewsMayNeedColorUpdate.add(R.id.rounded_corner_bottom_left); + viewsMayNeedColorUpdate.add(R.id.rounded_corner_bottom_right); + viewsMayNeedColorUpdate.add(R.id.display_cutout); + } + if (mFaceScanningFactory.getHasProviders()) { + viewsMayNeedColorUpdate.add(mFaceScanningViewId); + } + final Integer[] views = new Integer[viewsMayNeedColorUpdate.size()]; + viewsMayNeedColorUpdate.toArray(views); for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) { if (mOverlays[i] == null) { continue; @@ -914,12 +974,13 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab View child; for (int j = 0; j < size; j++) { child = overlayView.getChildAt(j); - if (child instanceof DisplayCutoutView) { + if (viewsMayNeedColorUpdate.contains(child.getId()) + && child instanceof DisplayCutoutView) { ((DisplayCutoutView) child).setColor(mTintColor); } } - mOverlays[i].onReloadResAndMeasure(roundedCornerIds, mProviderRefreshToken, mRotation, - mDisplayUniqueId); + mOverlays[i].onReloadResAndMeasure(views, mProviderRefreshToken, + mRotation, mDisplayUniqueId); } } @@ -976,7 +1037,15 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } pw.println(" mIsPrivacyDotEnabled:" + isPrivacyDotEnabled()); - pw.println(" isOnlyPrivacyDotInSwLayer:" + isOnlyPrivacyDotInSwLayer()); + pw.println(" shouldOptimizeOverlayVisibility:" + shouldOptimizeVisibility()); + final boolean supportsShowingFaceScanningAnim = mFaceScanningFactory.getHasProviders(); + pw.println(" supportsShowingFaceScanningAnim:" + supportsShowingFaceScanningAnim); + if (supportsShowingFaceScanningAnim) { + pw.println(" canShowFaceScanningAnim:" + + mFaceScanningFactory.canShowFaceScanningAnim()); + pw.println(" shouldShowFaceScanningAnim (at time dump was taken):" + + mFaceScanningFactory.shouldShowFaceScanningAnim()); + } pw.println(" mPendingConfigChange:" + mPendingConfigChange); if (mHwcScreenDecorationSupport != null) { pw.println(" mHwcScreenDecorationSupport:"); @@ -1045,6 +1114,16 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab // update all provider views inside overlay updateOverlayProviderViews(); } + + if (mFaceScanningFactory.getHasProviders()) { + FaceScanningOverlay faceScanningOverlay = + (FaceScanningOverlay) getOverlayView(mFaceScanningViewId); + if (faceScanningOverlay != null) { + faceScanningOverlay.setFaceScanningAnimColor( + Utils.getColorAttrDefaultColor(faceScanningOverlay.getContext(), + com.android.systemui.R.attr.wallpaperTextColorAccent)); + } + } } private boolean hasRoundedCorners() { @@ -1078,6 +1157,11 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab return isPrivacyDotEnabled() && isDefaultShownOverlayPos(pos, cutout); } + private boolean shouldShowSwLayerFaceScan(@BoundsPosition int pos, + @Nullable DisplayCutout cutout) { + return mFaceScanningFactory.getHasProviders() && isDefaultShownOverlayPos(pos, cutout); + } + private boolean shouldShowSwLayerCutout(@BoundsPosition int pos, @Nullable DisplayCutout cutout) { final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll(); @@ -1086,8 +1170,8 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab && mHwcScreenDecorationSupport == null); } - private boolean isOnlyPrivacyDotInSwLayer() { - return isPrivacyDotEnabled() + private boolean shouldOptimizeVisibility() { + return (isPrivacyDotEnabled() || mFaceScanningFactory.getHasProviders()) && (mHwcScreenDecorationSupport != null || (!hasRoundedCorners() && !shouldDrawCutout()) ); @@ -1201,9 +1285,9 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } public static class DisplayCutoutView extends DisplayCutoutBaseView { - private final List mBounds = new ArrayList(); - private final Rect mBoundingRect = new Rect(); - private Rect mTotalBounds = new Rect(); + final List mBounds = new ArrayList(); + final Rect mBoundingRect = new Rect(); + Rect mTotalBounds = new Rect(); private int mColor = Color.BLACK; private int mRotation; @@ -1213,6 +1297,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab public DisplayCutoutView(Context context, @BoundsPosition int pos) { super(context); mInitialPosition = pos; + paint.setColor(mColor); paint.setStyle(Paint.Style.FILL); setId(R.id.display_cutout); @@ -1309,7 +1394,7 @@ public class ScreenDecorations extends CoreStartable implements Tunable , Dumpab } if (showProtection) { - // Make sure that our measured height encompases the protection + // Make sure that our measured height encompasses the protection mTotalBounds.union(mBoundingRect); mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top, (int) protectionRect.right, (int) protectionRect.bottom); diff --git a/packages/SystemUI/src/com/android/systemui/decor/DecorProvider.kt b/packages/SystemUI/src/com/android/systemui/decor/DecorProvider.kt index 03ee8b11ab415..169b50edccfc8 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/DecorProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/DecorProvider.kt @@ -79,4 +79,16 @@ abstract class CornerDecorProvider : DecorProvider() { override val alignedBounds: List by lazy { listOf(alignedBound1, alignedBound2) } -} \ No newline at end of file +} + +/** + * A provider for view shown on bound. + */ +abstract class BoundDecorProvider : DecorProvider() { + /** The bound which a view is aligned based on rotation 0 */ + @DisplayCutout.BoundsPosition protected abstract val alignedBound: Int + + override val alignedBounds: List by lazy { + listOf(alignedBound) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt new file mode 100644 index 0000000000000..22a7cd15ee31b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/decor/FaceScanningProviderFactory.kt @@ -0,0 +1,181 @@ +/* + * 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.decor + +import android.content.Context +import android.util.Log +import android.view.DisplayCutout +import android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM +import android.view.DisplayCutout.BOUNDS_POSITION_LEFT +import android.view.DisplayCutout.BOUNDS_POSITION_RIGHT +import android.view.DisplayCutout.BOUNDS_POSITION_TOP +import android.view.DisplayInfo +import android.view.Gravity +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.FaceScanningOverlay +import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.statusbar.StatusBarStateController +import javax.inject.Inject + +@SysUISingleton +class FaceScanningProviderFactory @Inject constructor( + private val authController: AuthController, + private val context: Context, + private val statusBarStateController: StatusBarStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val featureFlags: FeatureFlags +) : DecorProviderFactory() { + private val display = context.display + private val displayInfo = DisplayInfo() + + override val hasProviders: Boolean + get() { + // update display info: + display?.getDisplayInfo(displayInfo) ?: run { + Log.w(TAG, "display is null, can't update displayInfo") + } + val hasDisplayCutout = DisplayCutout.getFillBuiltInDisplayCutout( + context.resources, displayInfo.uniqueId) + return hasDisplayCutout && + authController.faceAuthSensorLocation != null && + featureFlags.isEnabled(Flags.FACE_SCANNING_ANIM) + } + + override val providers: List + get() { + if (!hasProviders) { + return emptyList() + } + + return ArrayList().also { list -> + // displayInfo must be updated before using it; however it will already have + // been updated when accessing the hasProviders field above + displayInfo.displayCutout?.getBoundBaseOnCurrentRotation()?.let { bounds -> + // Add a face scanning view for each screen orientation. + // Cutout drawing is updated in ScreenDecorations#updateCutout + for (bound in bounds) { + list.add( + FaceScanningOverlayProviderImpl( + bound.baseOnRotation0(displayInfo.rotation), + authController, + statusBarStateController, + keyguardUpdateMonitor) + ) + } + } + } + } + + fun canShowFaceScanningAnim(): Boolean { + return hasProviders && keyguardUpdateMonitor.isFaceEnrolled + } + + fun shouldShowFaceScanningAnim(): Boolean { + return canShowFaceScanningAnim() && keyguardUpdateMonitor.isFaceScanning + } +} + +class FaceScanningOverlayProviderImpl( + override val alignedBound: Int, + private val authController: AuthController, + private val statusBarStateController: StatusBarStateController, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor +) : BoundDecorProvider() { + override val viewId: Int = com.android.systemui.R.id.face_scanning_anim + + override fun onReloadResAndMeasure( + view: View, + reloadToken: Int, + rotation: Int, + displayUniqueId: String? + ) { + // no need to handle rotation changes + } + + override fun inflateView( + context: Context, + parent: ViewGroup, + @Surface.Rotation rotation: Int + ): View { + val view = FaceScanningOverlay( + context, + alignedBound, + statusBarStateController, + keyguardUpdateMonitor) + view.id = viewId + view.visibility = View.INVISIBLE // only show this view when face scanning is happening + var heightLayoutParam = ViewGroup.LayoutParams.MATCH_PARENT + authController.faceAuthSensorLocation?.y?.let { + heightLayoutParam = (it * 3).toInt() + } + parent.addView(view, FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + heightLayoutParam, + Gravity.TOP or Gravity.START)) + return view + } +} + +fun DisplayCutout.getBoundBaseOnCurrentRotation(): List { + return ArrayList().also { + if (!boundingRectLeft.isEmpty) { + it.add(BOUNDS_POSITION_LEFT) + } + if (!boundingRectTop.isEmpty) { + it.add(BOUNDS_POSITION_TOP) + } + if (!boundingRectRight.isEmpty) { + it.add(BOUNDS_POSITION_RIGHT) + } + if (!boundingRectBottom.isEmpty) { + it.add(BOUNDS_POSITION_BOTTOM) + } + } +} + +fun Int.baseOnRotation0(@DisplayCutout.BoundsPosition currentRotation: Int): Int { + return when (currentRotation) { + Surface.ROTATION_0 -> this + Surface.ROTATION_90 -> when (this) { + BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_TOP + BOUNDS_POSITION_TOP -> BOUNDS_POSITION_RIGHT + BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_BOTTOM + else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_LEFT + } + Surface.ROTATION_270 -> when (this) { + BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_BOTTOM + BOUNDS_POSITION_TOP -> BOUNDS_POSITION_LEFT + BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_TOP + else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_RIGHT + } + else /* Surface.ROTATION_180 */ -> when (this) { + BOUNDS_POSITION_LEFT -> BOUNDS_POSITION_RIGHT + BOUNDS_POSITION_TOP -> BOUNDS_POSITION_BOTTOM + BOUNDS_POSITION_RIGHT -> BOUNDS_POSITION_LEFT + else /* BOUNDS_POSITION_BOTTOM */ -> BOUNDS_POSITION_TOP + } + } +} + +private const val TAG = "FaceScanningProvider" diff --git a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt index 136f135af7599..9f624b3dcb094 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/PrivacyDotDecorProviderFactory.kt @@ -28,6 +28,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import javax.inject.Inject +/** + * Provides privacy dot views for each orientation. The PrivacyDot orientation and visibility + * of the privacy dot views are controlled by the PrivacyDotViewController. + */ @SysUISingleton class PrivacyDotDecorProviderFactory @Inject constructor( @Main private val res: Resources diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index afa7d5e0a9c47..3c6253a85412a 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -80,6 +80,9 @@ public class Flags { public static final ResourceBooleanFlag BOUNCER_USER_SWITCHER = new ResourceBooleanFlag(204, R.bool.config_enableBouncerUserSwitcher); + public static final ResourceBooleanFlag FACE_SCANNING_ANIM = + new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation); + /***************************************/ // 300 - power menu public static final BooleanFlag POWER_MENU_LITE = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index 4f27fb4a9661e..d88f07ca304c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -195,7 +195,6 @@ class PrivacyDotViewController @Inject constructor( @UiThread private fun showDotView(dot: View, animate: Boolean) { - showingListener?.onPrivacyDotShown(dot) dot.clearAnimation() if (animate) { dot.visibility = View.VISIBLE @@ -209,6 +208,7 @@ class PrivacyDotViewController @Inject constructor( dot.visibility = View.VISIBLE dot.alpha = 1f } + showingListener?.onPrivacyDotShown(dot) } // Update the gravity and margins of the privacy views diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 92e49473927fd..a35efa995ddd1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -50,6 +50,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Insets; +import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -75,14 +76,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; +import com.android.keyguard.KeyguardUpdateMonitor; +import com.android.systemui.biometrics.AuthController; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.decor.CornerDecorProvider; import com.android.systemui.decor.DecorProvider; import com.android.systemui.decor.DecorProviderFactory; +import com.android.systemui.decor.FaceScanningOverlayProviderImpl; +import com.android.systemui.decor.FaceScanningProviderFactory; import com.android.systemui.decor.OverlayWindow; import com.android.systemui.decor.PrivacyDotCornerDecorProviderImpl; import com.android.systemui.decor.PrivacyDotDecorProviderFactory; import com.android.systemui.decor.RoundedCornerResDelegate; +import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.tuner.TunerService; @@ -113,6 +119,13 @@ public class ScreenDecorationsTest extends SysuiTestCase { private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); private FakeThreadFactory mThreadFactory; private ArrayList mPrivacyDecorProviders; + private ArrayList mFaceScanningProviders; + @Mock + private KeyguardUpdateMonitor mKeyguardUpdateMonitor; + @Mock + private StatusBarStateController mStatusBarStateController; + @Mock + private AuthController mAuthController; @Mock private Display mDisplay; @Mock @@ -128,6 +141,10 @@ public class ScreenDecorationsTest extends SysuiTestCase { @Mock private PrivacyDotDecorProviderFactory mPrivacyDotDecorProviderFactory; @Mock + private FaceScanningProviderFactory mFaceScanningProviderFactory; + @Mock + private FaceScanningOverlayProviderImpl mFaceScanningDecorProvider; + @Mock private CornerDecorProvider mPrivacyDotTopLeftDecorProvider; @Mock private CornerDecorProvider mPrivacyDotTopRightDecorProvider; @@ -189,9 +206,15 @@ public class ScreenDecorationsTest extends SysuiTestCase { DisplayCutout.BOUNDS_POSITION_RIGHT, R.layout.privacy_dot_bottom_right)); + mFaceScanningDecorProvider = spy(new FaceScanningOverlayProviderImpl( + BOUNDS_POSITION_TOP, + mAuthController, + mStatusBarStateController, + mKeyguardUpdateMonitor)); + mScreenDecorations = spy(new ScreenDecorations(mContext, mExecutor, mSecureSettings, mBroadcastDispatcher, mTunerService, mUserTracker, mDotViewController, - mThreadFactory, mPrivacyDotDecorProviderFactory) { + mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory) { @Override public void start() { super.start(); @@ -211,9 +234,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Override - protected void setOverlayWindowVisibilityIfViewExist(@Nullable View view, - @View.Visibility int visibility) { - super.setOverlayWindowVisibilityIfViewExist(view, visibility); + protected void updateOverlayWindowVisibilityIfViewExists(@Nullable View view) { + super.updateOverlayWindowVisibilityIfViewExists(view); mExecutor.runAllReady(); } }); @@ -269,6 +291,16 @@ public class ScreenDecorationsTest extends SysuiTestCase { } } + private void verifyFaceScanningViewExists(final boolean exists) { + final View overlay = mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP].getRootView(); + final View view = overlay.findViewById(mFaceScanningDecorProvider.getViewId()); + if (exists) { + assertNotNull(view); + } else { + assertNull(view); + } + } + @Nullable private View findViewFromOverlays(@IdRes int id) { for (OverlayWindow overlay: mScreenDecorations.mOverlays) { @@ -393,10 +425,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testNoRounding_NoCutout_NoPrivacyDot() { + public void testNoRounding_NoCutout_NoPrivacyDot_NoFaceScanning() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -411,10 +444,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testNoRounding_NoCutout_PrivacyDot() { + public void testNoRounding_NoCutout_PrivacyDot_NoFaceScanning() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -435,6 +469,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { // Privacy dots shall exist but invisible verifyDotViewsVisibility(View.INVISIBLE); + // Face scanning doesn't exist + verifyFaceScanningViewExists(false); + // One tunable. verify(mTunerService, times(1)).addTunable(any(), any()); // Dot controller init @@ -443,10 +480,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testRounding_NoCutout_NoPrivacyDot() { + public void testRounding_NoCutout_NoPrivacyDot_NoFaceScanning() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */); + 20 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -464,6 +502,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { // Privacy dots shall not exist verifyDotViewsNullable(true); + // Face scanning doesn't exist + verifyFaceScanningViewExists(false); + // One tunable. verify(mTunerService, times(1)).addTunable(any(), any()); // No dot controller init @@ -471,10 +512,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testRounding_NoCutout_PrivacyDot() { + public void testRounding_NoCutout_PrivacyDot_NoFaceScanning() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 20 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -494,6 +536,9 @@ public class ScreenDecorationsTest extends SysuiTestCase { // Privacy dots shall exist but invisible verifyDotViewsVisibility(View.INVISIBLE); + // Face scanning doesn't exist + verifyFaceScanningViewExists(false); + // One tunable. verify(mTunerService, times(1)).addTunable(any(), any()); // Dot controller init @@ -509,7 +554,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { /* roundedTopDrawable */, getTestsDrawable(com.android.systemui.tests.R.drawable.rounded3px) /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -527,7 +573,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { /* roundedTopDrawable */, getTestsDrawable(com.android.systemui.tests.R.drawable.rounded3px) /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -563,7 +610,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { /* roundedTopDrawable */, getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px) /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -598,7 +646,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNoRounding_CutoutShortEdge_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -621,7 +670,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNoRounding_CutoutShortEdge_PrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -655,7 +705,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNoRounding_CutoutLongEdge_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -682,7 +733,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNoRounding_CutoutLongEdge_PrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -709,7 +761,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testRounding_CutoutShortEdge_NoPrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 20 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -737,7 +790,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testRounding_CutoutShortEdge_PrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 20 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -768,7 +822,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testRounding_CutoutLongEdge_NoPrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 20 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -786,7 +841,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testRounding_CutoutLongEdge_PrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 20 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), null, null, null}; @@ -806,7 +862,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testRounding_CutoutShortAndLongEdge_NoPrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 20 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // top and left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), new Rect(9, 0, 10, 1), null, null}; @@ -825,7 +882,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testRounding_CutoutShortAndLongEdge_PrivacyDot() { setupResources(20 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 20 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 20 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // top and left cutout final Rect[] bounds = {new Rect(0, 50, 1, 60), new Rect(9, 0, 10, 1), null, null}; @@ -846,7 +904,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // Set to short edge cutout(top). final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -869,7 +928,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNoRounding_SwitchFrom_ShortEdgeCutout_To_LongCutout_PrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // Set to short edge cutout(top). final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -912,7 +972,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testDelayedCutout_NoPrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -935,7 +996,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testDelayedCutout_PrivacyDot() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -980,7 +1042,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { /* roundedTopDrawable */, getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px) /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning*/); mDisplayInfo.rotation = Surface.ROTATION_0; mScreenDecorations.start(); @@ -994,7 +1057,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { /* roundedTopDrawable */, getTestsDrawable(com.android.systemui.tests.R.drawable.rounded5px) /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning*/); mDisplayInfo.rotation = Surface.ROTATION_270; mScreenDecorations.onConfigurationChanged(null); @@ -1007,7 +1071,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testOnlyRoundedCornerRadiusTop() { setupResources(0 /* radius */, 10 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); mScreenDecorations.start(); @@ -1028,7 +1093,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testOnlyRoundedCornerRadiusBottom() { setupResources(0 /* radius */, 0 /* radiusTop */, 20 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); mScreenDecorations.start(); @@ -1099,7 +1165,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testSupportHwcLayer_SwitchFrom_NotSupport() { setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -1127,7 +1194,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testNotSupportHwcLayer_SwitchFrom_Support() { setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); decorationSupport.format = PixelFormat.R_8; doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); @@ -1165,7 +1233,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { /* roundedTopDrawable */, getTestsDrawable(com.android.systemui.tests.R.drawable.rounded4px) /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + true /* faceScanning */); final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); decorationSupport.format = PixelFormat.R_8; doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); @@ -1182,19 +1251,33 @@ public class ScreenDecorationsTest extends SysuiTestCase { // Make sure view found and window visibility changed as well final View view = mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView() .findViewById(R.id.privacy_dot_bottom_right_container); + view.setVisibility(View.VISIBLE); mPrivacyDotShowingListener.onPrivacyDotShown(view); assertEquals(View.VISIBLE, mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView().getVisibility()); + view.setVisibility(View.INVISIBLE); mPrivacyDotShowingListener.onPrivacyDotHidden(view); assertEquals(View.INVISIBLE, mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView().getVisibility()); + + // Make sure face scanning view found and window visibility updates on camera protection + // update + final View faceScanView = mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP].getRootView() + .findViewById(mFaceScanningDecorProvider.getViewId()); + when(mFaceScanningProviderFactory.shouldShowFaceScanningAnim()).thenReturn(true); + faceScanView.setVisibility(View.VISIBLE); + mScreenDecorations.showCameraProtection(new Path(), new Rect()); + mExecutor.runAllReady(); + assertEquals(View.VISIBLE, + mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP].getRootView().getVisibility()); } @Test public void testAutoShowHideOverlayWindowWhenNoRoundedAndNoCutout() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, true /* privacyDot */, + true /* faceScanning */); // no cutout doReturn(null).when(mScreenDecorations).getCutout(); @@ -1206,19 +1289,33 @@ public class ScreenDecorationsTest extends SysuiTestCase { // Make sure view found and window visibility changed as well final View view = mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView() .findViewById(R.id.privacy_dot_bottom_right_container); + view.setVisibility(View.VISIBLE); mPrivacyDotShowingListener.onPrivacyDotShown(view); assertEquals(View.VISIBLE, mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView().getVisibility()); + view.setVisibility(View.INVISIBLE); mPrivacyDotShowingListener.onPrivacyDotHidden(view); assertEquals(View.INVISIBLE, mScreenDecorations.mOverlays[BOUNDS_POSITION_BOTTOM].getRootView().getVisibility()); + + // Make sure face scanning view found and window visibility updates on camera protection + // update + final View faceScanView = mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP].getRootView() + .findViewById(mFaceScanningDecorProvider.getViewId()); + faceScanView.setVisibility(View.VISIBLE); + when(mFaceScanningProviderFactory.shouldShowFaceScanningAnim()).thenReturn(true); + mScreenDecorations.showCameraProtection(new Path(), new Rect()); + mExecutor.runAllReady(); + assertEquals(View.VISIBLE, + mScreenDecorations.mOverlays[BOUNDS_POSITION_TOP].getRootView().getVisibility()); } @Test - public void testHwcLayer_noPrivacyDot() { + public void testHwcLayer_noPrivacyDot_noFaceScanning() { setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); decorationSupport.format = PixelFormat.R_8; doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); @@ -1236,10 +1333,11 @@ public class ScreenDecorationsTest extends SysuiTestCase { } @Test - public void testHwcLayer_PrivacyDot() { + public void testHwcLayer_PrivacyDot_FaceScanning() { setupResources(0 /* radius */, 10 /* radiusTop */, 20 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + true /* faceScanning */); final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); decorationSupport.format = PixelFormat.R_8; doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); @@ -1257,13 +1355,16 @@ public class ScreenDecorationsTest extends SysuiTestCase { verify(mDotViewController, times(1)).initialize(any(), any(), any(), any()); verify(mDotViewController, times(1)).setShowingListener( mScreenDecorations.mPrivacyDotShowingListener); + + verifyFaceScanningViewExists(true); } @Test public void testOnDisplayChanged_hwcLayer() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); final DisplayDecorationSupport decorationSupport = new DisplayDecorationSupport(); decorationSupport.format = PixelFormat.R_8; doReturn(decorationSupport).when(mDisplay).getDisplayDecorationSupport(); @@ -1288,7 +1389,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testOnDisplayChanged_nonHwcLayer() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); // top cutout final Rect[] bounds = {null, new Rect(9, 0, 10, 1), null, null}; @@ -1311,7 +1413,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testHasSameProvidersWithNullOverlays() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */); + 0 /* roundedPadding */, false /* fillCutout */, false /* privacyDot */, + false /* faceScanning */); mScreenDecorations.start(); @@ -1329,7 +1432,8 @@ public class ScreenDecorationsTest extends SysuiTestCase { public void testHasSameProvidersWithPrivacyDots() { setupResources(0 /* radius */, 0 /* radiusTop */, 0 /* radiusBottom */, null /* roundedTopDrawable */, null /* roundedBottomDrawable */, - 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */); + 0 /* roundedPadding */, true /* fillCutout */, true /* privacyDot */, + false /* faceScanning */); mScreenDecorations.start(); @@ -1366,7 +1470,7 @@ public class ScreenDecorationsTest extends SysuiTestCase { private void setupResources(int radius, int radiusTop, int radiusBottom, @Nullable Drawable roundedTopDrawable, @Nullable Drawable roundedBottomDrawable, - int roundedPadding, boolean fillCutout, boolean privacyDot) { + int roundedPadding, boolean fillCutout, boolean privacyDot, boolean faceScanning) { mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.array.config_displayUniqueIdArray, new String[]{}); @@ -1418,6 +1522,13 @@ public class ScreenDecorationsTest extends SysuiTestCase { } when(mPrivacyDotDecorProviderFactory.getProviders()).thenReturn(mPrivacyDecorProviders); when(mPrivacyDotDecorProviderFactory.getHasProviders()).thenReturn(privacyDot); + + mFaceScanningProviders = new ArrayList<>(); + if (faceScanning) { + mFaceScanningProviders.add(mFaceScanningDecorProvider); + } + when(mFaceScanningProviderFactory.getProviders()).thenReturn(mFaceScanningProviders); + when(mFaceScanningProviderFactory.getHasProviders()).thenReturn(faceScanning); } private DisplayCutout getDisplayCutoutForRotation(Insets safeInsets, Rect[] cutoutBounds) {