From 05c83b715a463388d45f2c0aaff5b4225541e1c4 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Mon, 29 Nov 2021 11:58:48 -0800 Subject: [PATCH 1/3] Migrate small UDFPS classes to kotlin and backfill missing tests. Bug: 205875955 Test: atest UdfpsViewTest UdfpsControllerTest Test: manual (enroll manually and BP test app) Change-Id: Ia09f18bffe943ecdf7a73c71faae6f2e2d40e359 --- .../biometrics/UdfpsAnimationView.java | 2 +- .../UdfpsAnimationViewController.java | 202 ------------- .../UdfpsAnimationViewController.kt | 170 +++++++++++ .../{UdfpsBpView.java => UdfpsBpView.kt} | 26 +- ...ntroller.java => UdfpsBpViewController.kt} | 42 ++- .../systemui/biometrics/UdfpsDrawable.java | 113 -------- .../systemui/biometrics/UdfpsDrawable.kt | 104 +++++++ .../biometrics/UdfpsEnrollDrawable.java | 12 +- .../biometrics/UdfpsEnrollViewController.java | 2 +- ...dfpsFpDrawable.java => UdfpsFpDrawable.kt} | 26 +- .../biometrics/UdfpsFpmOtherView.java | 49 ---- .../systemui/biometrics/UdfpsFpmOtherView.kt | 40 +++ ...er.java => UdfpsFpmOtherViewController.kt} | 41 ++- .../UdfpsKeyguardViewController.java | 18 +- .../systemui/biometrics/UdfpsView.java | 274 ------------------ .../android/systemui/biometrics/UdfpsView.kt | 220 ++++++++++++++ .../biometrics/BiometricTestExtensions.kt | 42 +++ .../biometrics/UdfpsControllerTest.java | 12 +- .../systemui/biometrics/UdfpsViewTest.kt | 172 +++++++++++ 19 files changed, 829 insertions(+), 738 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt rename packages/SystemUI/src/com/android/systemui/biometrics/{UdfpsBpView.java => UdfpsBpView.kt} (52%) rename packages/SystemUI/src/com/android/systemui/biometrics/{UdfpsBpViewController.java => UdfpsBpViewController.kt} (52%) delete mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.kt rename packages/SystemUI/src/com/android/systemui/biometrics/{UdfpsFpDrawable.java => UdfpsFpDrawable.kt} (61%) delete mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherView.java create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherView.kt rename packages/SystemUI/src/com/android/systemui/biometrics/{UdfpsFpmOtherViewController.java => UdfpsFpmOtherViewController.kt} (54%) delete mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.java create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java index e7f637421a4de..7fdb5eab8701a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationView.java @@ -33,7 +33,7 @@ import android.widget.FrameLayout; * - sends sensor rect updates to fingerprint drawable * - optionally can override dozeTimeTick to adjust views for burn-in mitigation */ -abstract class UdfpsAnimationView extends FrameLayout { +public abstract class UdfpsAnimationView extends FrameLayout { // mAlpha takes into consideration the status bar expansion amount to fade out icon when // the status bar is expanded private int mAlpha; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java deleted file mode 100644 index 73e3aecd8b631..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2021 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.biometrics; - -import android.annotation.NonNull; -import android.graphics.PointF; -import android.graphics.RectF; - -import com.android.systemui.Dumpable; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; -import com.android.systemui.util.ViewController; - -import java.io.FileDescriptor; -import java.io.PrintWriter; - -/** - * Handles: - * 1. registering for listeners when its view is attached and unregistering on view detached - * 2. pausing udfps when fingerprintManager may still be running but we temporarily want to hide - * the affordance. this allows us to fade the view in and out nicely (see shouldPauseAuth) - * 3. sending events to its view including: - * - illumination events - * - sensor position changes - * - doze time event - */ -abstract class UdfpsAnimationViewController - extends ViewController implements Dumpable { - @NonNull final StatusBarStateController mStatusBarStateController; - @NonNull final PanelExpansionStateManager mPanelExpansionStateManager; - @NonNull final SystemUIDialogManager mDialogManager; - @NonNull final DumpManager mDumpManger; - - boolean mNotificationShadeVisible; - - protected UdfpsAnimationViewController( - T view, - @NonNull StatusBarStateController statusBarStateController, - @NonNull PanelExpansionStateManager panelExpansionStateManager, - @NonNull SystemUIDialogManager dialogManager, - @NonNull DumpManager dumpManager) { - super(view); - mStatusBarStateController = statusBarStateController; - mPanelExpansionStateManager = panelExpansionStateManager; - mDialogManager = dialogManager; - mDumpManger = dumpManager; - } - - abstract @NonNull String getTag(); - - @Override - protected void onViewAttached() { - mPanelExpansionStateManager.addExpansionListener(mPanelExpansionListener); - mDialogManager.registerListener(mDialogListener); - mDumpManger.registerDumpable(getDumpTag(), this); - } - - @Override - protected void onViewDetached() { - mPanelExpansionStateManager.removeExpansionListener(mPanelExpansionListener); - mDialogManager.unregisterListener(mDialogListener); - mDumpManger.unregisterDumpable(getDumpTag()); - } - - /** - * in some cases, onViewAttached is called for the newly added view using an instance of - * this controller before onViewDetached is called on the previous view, so we must have a - * unique dump tag per instance of this class - * @return a unique tag for this instance of this class - */ - private String getDumpTag() { - return getTag() + " (" + this + ")"; - } - - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - pw.println("mNotificationShadeVisible=" + mNotificationShadeVisible); - pw.println("shouldPauseAuth()=" + shouldPauseAuth()); - pw.println("isPauseAuth=" + mView.isPauseAuth()); - } - - /** - * Returns true if the fingerprint manager is running but we want to temporarily pause - * authentication. - */ - boolean shouldPauseAuth() { - return mNotificationShadeVisible - || mDialogManager.shouldHideAffordance(); - } - - /** - * Send pause auth update to our view. - */ - void updatePauseAuth() { - if (mView.setPauseAuth(shouldPauseAuth())) { - mView.postInvalidate(); - } - } - - /** - * Send sensor position change to our view. This rect contains paddingX and paddingY. - */ - void onSensorRectUpdated(RectF sensorRect) { - mView.onSensorRectUpdated(sensorRect); - } - - /** - * Send dozeTimeTick to view in case it wants to handle its burn-in offset. - */ - void dozeTimeTick() { - if (mView.dozeTimeTick()) { - mView.postInvalidate(); - } - } - - /** - * @return the amount of translation needed if the view currently requires the user to touch - * somewhere other than the exact center of the sensor. For example, this can happen - * during guided enrollment. - */ - PointF getTouchTranslation() { - return new PointF(0, 0); - } - - /** - * X-Padding to add to left and right of the sensor rectangle area to increase the size of our - * window to draw within. - * @return - */ - int getPaddingX() { - return 0; - } - - /** - * Y-Padding to add to top and bottom of the sensor rectangle area to increase the size of our - * window to draw within. - */ - int getPaddingY() { - return 0; - } - - /** - * Udfps has started illuminating and the fingerprint manager is working on authenticating. - */ - void onIlluminationStarting() { - mView.onIlluminationStarting(); - mView.postInvalidate(); - } - - /** - * Udfps has stopped illuminating and the fingerprint manager is no longer attempting to - * authenticate. - */ - void onIlluminationStopped() { - mView.onIlluminationStopped(); - mView.postInvalidate(); - } - - /** - * Whether to listen for touches outside of the view. - */ - boolean listenForTouchesOutsideView() { - return false; - } - - /** - * Called on touches outside of the view if listenForTouchesOutsideView returns true - */ - void onTouchOutsideView() { } - - private final PanelExpansionListener mPanelExpansionListener = new PanelExpansionListener() { - @Override - public void onPanelExpansionChanged( - float fraction, boolean expanded, boolean tracking) { - // Notification shade can be expanded but not visible (fraction: 0.0), for example - // when a heads-up notification (HUN) is showing. - mNotificationShadeVisible = expanded && fraction > 0f; - mView.onExpansionChanged(fraction); - updatePauseAuth(); - } - }; - - private final SystemUIDialogManager.Listener mDialogListener = - (shouldHide) -> updatePauseAuth(); -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt new file mode 100644 index 0000000000000..c33cd8d537dce --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2021 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.biometrics + +import android.graphics.PointF +import android.graphics.RectF +import com.android.systemui.Dumpable +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener +import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager +import com.android.systemui.util.ViewController +import java.io.FileDescriptor +import java.io.PrintWriter + +/** + * Handles: + * 1. registering for listeners when its view is attached and unregistering on view detached + * 2. pausing udfps when fingerprintManager may still be running but we temporarily want to hide + * the affordance. this allows us to fade the view in and out nicely (see shouldPauseAuth) + * 3. sending events to its view including: + * - illumination events + * - sensor position changes + * - doze time event + */ +abstract class UdfpsAnimationViewController( + view: T, + protected val statusBarStateController: StatusBarStateController, + protected val panelExpansionStateManager: PanelExpansionStateManager, + protected val dialogManager: SystemUIDialogManager, + private val dumpManager: DumpManager +) : ViewController(view), Dumpable { + + protected abstract val tag: String + + private val view: T + get() = mView!! + + private val dialogListener = SystemUIDialogManager.Listener { updatePauseAuth() } + + private val panelExpansionListener = + PanelExpansionListener { fraction, expanded, tracking -> + // Notification shade can be expanded but not visible (fraction: 0.0), for example + // when a heads-up notification (HUN) is showing. + notificationShadeVisible = expanded && fraction > 0f + view.onExpansionChanged(fraction) + updatePauseAuth() + } + + /** If the notification shade is visible. */ + var notificationShadeVisible: Boolean = false + + /** + * The amount of translation needed if the view currently requires the user to touch + * somewhere other than the exact center of the sensor. For example, this can happen + * during guided enrollment. + */ + open val touchTranslation: PointF = PointF(0f, 0f) + + /** + * X-Padding to add to left and right of the sensor rectangle area to increase the size of our + * window to draw within. + */ + open val paddingX: Int = 0 + + /** + * Y-Padding to add to top and bottom of the sensor rectangle area to increase the size of our + * window to draw within. + */ + open val paddingY: Int = 0 + + override fun onViewAttached() { + panelExpansionStateManager.addExpansionListener(panelExpansionListener) + dialogManager.registerListener(dialogListener) + dumpManager.registerDumpable(dumpTag, this) + } + + override fun onViewDetached() { + panelExpansionStateManager.removeExpansionListener(panelExpansionListener) + dialogManager.unregisterListener(dialogListener) + dumpManager.unregisterDumpable(dumpTag) + } + + /** + * in some cases, onViewAttached is called for the newly added view using an instance of + * this controller before onViewDetached is called on the previous view, so we must have a + * unique [dumpTag] per instance of this class. + */ + private val dumpTag = "$tag ($this)" + + override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array) { + pw.println("mNotificationShadeVisible=$notificationShadeVisible") + pw.println("shouldPauseAuth()=" + shouldPauseAuth()) + pw.println("isPauseAuth=" + view.isPauseAuth) + } + + /** + * Returns true if the fingerprint manager is running, but we want to temporarily pause + * authentication. + */ + open fun shouldPauseAuth(): Boolean { + return notificationShadeVisible || dialogManager.shouldHideAffordance() + } + + /** + * Send pause auth update to our view. + */ + fun updatePauseAuth() { + if (view.setPauseAuth(shouldPauseAuth())) { + view.postInvalidate() + } + } + + /** + * Send sensor position change to our view. This rect contains paddingX and paddingY. + */ + fun onSensorRectUpdated(sensorRect: RectF) { + view.onSensorRectUpdated(sensorRect) + } + + /** + * Send dozeTimeTick to view in case it wants to handle its burn-in offset. + */ + fun dozeTimeTick() { + if (view.dozeTimeTick()) { + view.postInvalidate() + } + } + + /** + * Udfps has started illuminating and the fingerprint manager is working on authenticating. + */ + fun onIlluminationStarting() { + view.onIlluminationStarting() + view.postInvalidate() + } + + /** + * Udfps has stopped illuminating and the fingerprint manager is no longer attempting to + * authenticate. + */ + fun onIlluminationStopped() { + view.onIlluminationStopped() + view.postInvalidate() + } + + /** + * Whether to listen for touches outside of the view. + */ + open fun listenForTouchesOutsideView(): Boolean = false + + /** + * Called on touches outside of the view if listenForTouchesOutsideView returns true + */ + open fun onTouchOutsideView() {} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt similarity index 52% rename from packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.java rename to packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt index 70be907228c85..6607915fac9d9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpView.kt @@ -13,33 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.systemui.biometrics -package com.android.systemui.biometrics; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.annotation.Nullable; +import android.content.Context +import android.util.AttributeSet /** * Class that coordinates non-HBM animations during BiometricPrompt. * * Currently doesn't draw anything. * - * Note that {@link AuthBiometricUdfpsView} also shows UDFPS animations. At some point we should + * Note that [AuthBiometricUdfpsView] also shows UDFPS animations. At some point we should * de-dupe this if necessary. */ -public class UdfpsBpView extends UdfpsAnimationView { - private UdfpsFpDrawable mFingerprintDrawable; +class UdfpsBpView(context: Context, attrs: AttributeSet?) : UdfpsAnimationView(context, attrs) { - public UdfpsBpView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - // Drawable isn't ever added to the view, so we don't currently show anything - mFingerprintDrawable = new UdfpsFpDrawable(mContext); - } + // Drawable isn't ever added to the view, so we don't currently show anything + private val fingerprintDrawable: UdfpsFpDrawable = UdfpsFpDrawable(context) - @Override - UdfpsDrawable getDrawable() { - return mFingerprintDrawable; - } + override fun getDrawable(): UdfpsDrawable = fingerprintDrawable } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt similarity index 52% rename from packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java rename to packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt index 3732100a2a060..4cd40d2f186b3 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt @@ -13,32 +13,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.android.systemui.biometrics -package com.android.systemui.biometrics; - -import android.annotation.NonNull; - -import com.android.systemui.dump.DumpManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.statusbar.phone.SystemUIDialogManager; -import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager; +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager /** * Class that coordinates non-HBM animations for biometric prompt. */ -class UdfpsBpViewController extends UdfpsAnimationViewController { - protected UdfpsBpViewController( - @NonNull UdfpsBpView view, - @NonNull StatusBarStateController statusBarStateController, - @NonNull PanelExpansionStateManager panelExpansionStateManager, - @NonNull SystemUIDialogManager systemUIDialogManager, - @NonNull DumpManager dumpManager) { - super(view, statusBarStateController, panelExpansionStateManager, - systemUIDialogManager, dumpManager); - } - - @Override - @NonNull String getTag() { - return "UdfpsBpViewController"; - } +class UdfpsBpViewController( + view: UdfpsBpView, + statusBarStateController: StatusBarStateController, + panelExpansionStateManager: PanelExpansionStateManager, + systemUIDialogManager: SystemUIDialogManager, + dumpManager: DumpManager +) : UdfpsAnimationViewController( + view, + statusBarStateController, + panelExpansionStateManager, + systemUIDialogManager, + dumpManager +) { + override val tag = "UdfpsBpViewController" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java deleted file mode 100644 index 55ed5aaff9588..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2021 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.biometrics; - -import android.content.Context; -import android.graphics.ColorFilter; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.PathShape; -import android.util.PathParser; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.systemui.R; - -/** - * Abstract base class for drawable displayed when the finger is not touching the - * sensor area. - */ -public abstract class UdfpsDrawable extends Drawable { - static final float DEFAULT_STROKE_WIDTH = 3f; - - @NonNull final Context mContext; - @NonNull final ShapeDrawable mFingerprintDrawable; - private final Paint mPaint; - private boolean mIlluminationShowing; - - int mAlpha = 255; // 0 - 255 - public UdfpsDrawable(@NonNull Context context) { - mContext = context; - final String fpPath = context.getResources().getString(R.string.config_udfpsIcon); - mFingerprintDrawable = new ShapeDrawable( - new PathShape(PathParser.createPathFromPathData(fpPath), 72, 72)); - mFingerprintDrawable.mutate(); - - mPaint = mFingerprintDrawable.getPaint(); - mPaint.setStyle(Paint.Style.STROKE); - mPaint.setStrokeCap(Paint.Cap.ROUND); - setStrokeWidth(DEFAULT_STROKE_WIDTH); - } - - void setStrokeWidth(float strokeWidth) { - mPaint.setStrokeWidth(strokeWidth); - invalidateSelf(); - } - - /** - * @param sensorRect the rect coordinates for the sensor area - */ - public void onSensorRectUpdated(@NonNull RectF sensorRect) { - final int margin = (int) sensorRect.height() / 8; - - final Rect bounds = new Rect((int) sensorRect.left + margin, - (int) sensorRect.top + margin, - (int) sensorRect.right - margin, - (int) sensorRect.bottom - margin); - updateFingerprintIconBounds(bounds); - } - - /** - * Bounds for the fingerprint icon - */ - protected void updateFingerprintIconBounds(@NonNull Rect bounds) { - mFingerprintDrawable.setBounds(bounds); - invalidateSelf(); - } - - @Override - public void setAlpha(int alpha) { - mAlpha = alpha; - mFingerprintDrawable.setAlpha(mAlpha); - invalidateSelf(); - } - - boolean isIlluminationShowing() { - return mIlluminationShowing; - } - - void setIlluminationShowing(boolean showing) { - if (mIlluminationShowing == showing) { - return; - } - mIlluminationShowing = showing; - invalidateSelf(); - } - - @Override - public void setColorFilter(@Nullable ColorFilter colorFilter) { - } - - @Override - public int getOpacity() { - return 0; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.kt new file mode 100644 index 0000000000000..ee112b47e2433 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDrawable.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 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.biometrics + +import android.content.Context +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.Drawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.PathShape +import android.util.PathParser +import com.android.systemui.R + +private const val DEFAULT_STROKE_WIDTH = 3f + +/** + * Abstract base class for drawable displayed when the finger is not touching the + * sensor area. + */ +abstract class UdfpsDrawable( + protected val context: Context, + drawableFactory: (Context) -> ShapeDrawable +) : Drawable() { + + constructor(context: Context) : this(context, defaultFactory) + + /** Fingerprint affordance. */ + val fingerprintDrawable: ShapeDrawable = drawableFactory(context) + + private var _alpha: Int = 255 // 0 - 255 + + var strokeWidth: Float = fingerprintDrawable.paint.strokeWidth + set(value) { + field = value + fingerprintDrawable.paint.strokeWidth = value + invalidateSelf() + } + + var isIlluminationShowing: Boolean = false + set(showing) { + if (field == showing) { + return + } + field = showing + invalidateSelf() + } + + /** The [sensorRect] coordinates for the sensor area. */ + open fun onSensorRectUpdated(sensorRect: RectF) { + val margin = sensorRect.height().toInt() / 8 + val bounds = Rect( + sensorRect.left.toInt() + margin, + sensorRect.top.toInt() + margin, + sensorRect.right.toInt() - margin, + sensorRect.bottom.toInt() - margin + ) + updateFingerprintIconBounds(bounds) + } + + /** Bounds for the fingerprint icon. */ + protected open fun updateFingerprintIconBounds(bounds: Rect) { + fingerprintDrawable.bounds = bounds + invalidateSelf() + } + + override fun getAlpha(): Int = _alpha + + override fun setAlpha(alpha: Int) { + _alpha = alpha + fingerprintDrawable.alpha = alpha + invalidateSelf() + } + + override fun setColorFilter(colorFilter: ColorFilter?) {} + + override fun getOpacity(): Int = 0 +} + +private val defaultFactory = { context: Context -> + val fpPath = context.resources.getString(R.string.config_udfpsIcon) + val drawable = ShapeDrawable( + PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f) + ) + drawable.mutate() + drawable.paint.style = Paint.Style.STROKE + drawable.paint.strokeCap = Paint.Cap.ROUND + drawable.paint.strokeWidth = DEFAULT_STROKE_WIDTH + drawable +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java index ac38b13cc4dd4..e7015115d84cd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollDrawable.java @@ -102,7 +102,7 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { mSensorOutlinePaint = new Paint(0 /* flags */); mSensorOutlinePaint.setAntiAlias(true); - mSensorOutlinePaint.setColor(mContext.getColor(R.color.udfps_moving_target_fill)); + mSensorOutlinePaint.setColor(context.getColor(R.color.udfps_moving_target_fill)); mSensorOutlinePaint.setStyle(Paint.Style.FILL); mBlueFill = new Paint(0 /* flags */); @@ -112,10 +112,10 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { mMovingTargetFpIcon = context.getResources() .getDrawable(R.drawable.ic_kg_fingerprint, null); - mMovingTargetFpIcon.setTint(mContext.getColor(R.color.udfps_enroll_icon)); + mMovingTargetFpIcon.setTint(context.getColor(R.color.udfps_enroll_icon)); mMovingTargetFpIcon.mutate(); - mFingerprintDrawable.setTint(mContext.getColor(R.color.udfps_enroll_icon)); + getFingerprintDrawable().setTint(context.getColor(R.color.udfps_enroll_icon)); mHintColorFaded = context.getColor(R.color.udfps_moving_target_fill); mHintColorHighlight = context.getColor(R.color.udfps_enroll_progress); @@ -404,9 +404,9 @@ public class UdfpsEnrollDrawable extends UdfpsDrawable { if (mSensorRect != null) { canvas.drawOval(mSensorRect, mSensorOutlinePaint); } - mFingerprintDrawable.draw(canvas); - mFingerprintDrawable.setAlpha(mAlpha); - mSensorOutlinePaint.setAlpha(mAlpha); + getFingerprintDrawable().draw(canvas); + getFingerprintDrawable().setAlpha(getAlpha()); + mSensorOutlinePaint.setAlpha(getAlpha()); } // Draw the finger tip or edges hint. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java index ac9e92e22569b..2ca103bf942f6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsEnrollViewController.java @@ -66,7 +66,7 @@ public class UdfpsEnrollViewController extends UdfpsAnimationViewController { - protected UdfpsFpmOtherViewController( - @NonNull UdfpsFpmOtherView view, - @NonNull StatusBarStateController statusBarStateController, - @NonNull PanelExpansionStateManager panelExpansionStateManager, - @NonNull SystemUIDialogManager systemUIDialogManager, - @NonNull DumpManager dumpManager) { - super(view, statusBarStateController, panelExpansionStateManager, systemUIDialogManager, - dumpManager); - } - - @Override - @NonNull String getTag() { - return "UdfpsFpmOtherViewController"; - } +class UdfpsFpmOtherViewController( + view: UdfpsFpmOtherView, + statusBarStateController: StatusBarStateController, + panelExpansionStateManager: PanelExpansionStateManager, + systemUIDialogManager: SystemUIDialogManager, + dumpManager: DumpManager +) : UdfpsAnimationViewController( + view, + statusBarStateController, + panelExpansionStateManager, + systemUIDialogManager, + dumpManager +) { + override val tag = "UdfpsFpmOtherViewController" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java index 3e8a568cb8036..d90a746b0f79c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java @@ -102,7 +102,7 @@ public class UdfpsKeyguardViewController extends UdfpsAnimationViewController (cx - rx * mSensorTouchAreaCoefficient) - && x < (cx + rx * mSensorTouchAreaCoefficient) - && y > (cy - ry * mSensorTouchAreaCoefficient) - && y < (cy + ry * mSensorTouchAreaCoefficient) - && !mAnimationViewController.shouldPauseAuth(); - } - - boolean isIlluminationRequested() { - return mIlluminationRequested; - } - - /** - * @param onIlluminatedRunnable Runs when the first illumination frame reaches the panel. - */ - @Override - public void startIllumination(@Nullable Runnable onIlluminatedRunnable) { - mIlluminationRequested = true; - if (mAnimationViewController != null) { - mAnimationViewController.onIlluminationStarting(); - } - - if (mGhbmView != null) { - mGhbmView.setGhbmIlluminationListener(this::doIlluminate); - mGhbmView.setVisibility(View.VISIBLE); - mGhbmView.startGhbmIllumination(onIlluminatedRunnable); - } else { - doIlluminate(null /* surface */, onIlluminatedRunnable); - } - } - - private void doIlluminate(@Nullable Surface surface, @Nullable Runnable onIlluminatedRunnable) { - if (mGhbmView != null && surface == null) { - Log.e(TAG, "doIlluminate | surface must be non-null for GHBM"); - } - if (mHbmProvider != null) { - mHbmProvider.enableHbm(mHbmType, surface, () -> { - if (mGhbmView != null) { - mGhbmView.drawIlluminationDot(mSensorRect); - } - if (onIlluminatedRunnable != null) { - // No framework API can reliably tell when a frame reaches the panel. A timeout - // is the safest solution. - postDelayed(onIlluminatedRunnable, mOnIlluminatedDelayMs); - } else { - Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null"); - } - }); - } - } - - @Override - public void stopIllumination() { - mIlluminationRequested = false; - if (mAnimationViewController != null) { - mAnimationViewController.onIlluminationStopped(); - } - if (mGhbmView != null) { - mGhbmView.setGhbmIlluminationListener(null); - mGhbmView.setVisibility(View.INVISIBLE); - } - if (mHbmProvider != null) { - mHbmProvider.disableHbm(null /* onHbmDisabled */); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt new file mode 100644 index 0000000000000..9fbc458cb0824 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsView.kt @@ -0,0 +1,220 @@ +/* + * 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.biometrics + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PointF +import android.graphics.RectF +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.os.Build +import android.os.UserHandle +import android.provider.Settings +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.Surface +import android.widget.FrameLayout +import com.android.systemui.R +import com.android.systemui.doze.DozeReceiver +import com.android.systemui.biometrics.UdfpsHbmTypes.HbmType + +private const val TAG = "UdfpsView" +private const val SETTING_HBM_TYPE = "com.android.systemui.biometrics.UdfpsSurfaceView.hbmType" +@HbmType +private const val DEFAULT_HBM_TYPE = UdfpsHbmTypes.LOCAL_HBM + +/** + * A view containing 1) A SurfaceView for HBM, and 2) A normal drawable view for all other + * animations. + */ +class UdfpsView( + context: Context, + attrs: AttributeSet? +) : FrameLayout(context, attrs), DozeReceiver, UdfpsIlluminator { + + private val sensorRect = RectF() + private var hbmProvider: UdfpsHbmProvider? = null + private val debugTextPaint = Paint().apply { + isAntiAlias = true + color = Color.BLUE + textSize = 32f + } + + private val sensorTouchAreaCoefficient: Float = + context.theme.obtainStyledAttributes(attrs, R.styleable.UdfpsView, 0, 0).use { a -> + require(a.hasValue(R.styleable.UdfpsView_sensorTouchAreaCoefficient)) { + "UdfpsView must contain sensorTouchAreaCoefficient" + } + a.getFloat(R.styleable.UdfpsView_sensorTouchAreaCoefficient, 0f) + } + + private val onIlluminatedDelayMs = context.resources.getInteger( + com.android.internal.R.integer.config_udfps_illumination_transition_ms + ).toLong() + + @HbmType + private val hbmType = if (Build.IS_ENG || Build.IS_USERDEBUG) { + Settings.Secure.getIntForUser( + context.contentResolver, + SETTING_HBM_TYPE, + DEFAULT_HBM_TYPE, + UserHandle.USER_CURRENT + ) + } else { + DEFAULT_HBM_TYPE + } + + // Only used for UdfpsHbmTypes.GLOBAL_HBM. + private var ghbmView: UdfpsSurfaceView? = null + + /** View controller (can be different for enrollment, BiometricPrompt, Keyguard, etc.). */ + var animationViewController: UdfpsAnimationViewController<*>? = null + + /** Properties used to obtain the sensor location. */ + var sensorProperties: FingerprintSensorPropertiesInternal? = null + + /** Debug message. */ + var debugMessage: String? = null + set(value) { + field = value + postInvalidate() + } + + /** When [startIllumination] has been called but not stopped via [stopIllumination]. */ + var isIlluminationRequested: Boolean = false + private set + + override fun setHbmProvider(provider: UdfpsHbmProvider?) { + hbmProvider = provider + } + + // Don't propagate any touch events to the child views. + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + return (animationViewController == null || !animationViewController!!.shouldPauseAuth()) + } + + override fun onFinishInflate() { + if (hbmType == UdfpsHbmTypes.GLOBAL_HBM) { + ghbmView = findViewById(R.id.hbm_view) + } + } + + override fun dozeTimeTick() { + animationViewController?.dozeTimeTick() + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + + val paddingX = animationViewController?.paddingX ?: 0 + val paddingY = animationViewController?.paddingY ?: 0 + val sensorRadius = sensorProperties?.location?.sensorRadius ?: 0 + + sensorRect.set( + paddingX.toFloat(), + paddingY.toFloat(), + (2 * sensorRadius + paddingX).toFloat(), + (2 * sensorRadius + paddingY).toFloat() + ) + animationViewController?.onSensorRectUpdated(RectF(sensorRect)) + } + + fun onTouchOutsideView() { + animationViewController?.onTouchOutsideView() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + Log.v(TAG, "onAttachedToWindow") + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + Log.v(TAG, "onDetachedFromWindow") + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (!isIlluminationRequested) { + if (!debugMessage.isNullOrEmpty()) { + canvas.drawText(debugMessage!!, 0f, 160f, debugTextPaint) + } + } + } + + fun isWithinSensorArea(x: Float, y: Float): Boolean { + // The X and Y coordinates of the sensor's center. + val translation = animationViewController?.touchTranslation ?: PointF(0f, 0f) + val cx = sensorRect.centerX() + translation.x + val cy = sensorRect.centerY() + translation.y + // Radii along the X and Y axes. + val rx = (sensorRect.right - sensorRect.left) / 2.0f + val ry = (sensorRect.bottom - sensorRect.top) / 2.0f + + return x > cx - rx * sensorTouchAreaCoefficient && + x < cx + rx * sensorTouchAreaCoefficient && + y > cy - ry * sensorTouchAreaCoefficient && + y < cy + ry * sensorTouchAreaCoefficient && + !(animationViewController?.shouldPauseAuth() ?: false) + } + + /** + * Start and run [onIlluminatedRunnable] when the first illumination frame reaches the panel. + */ + override fun startIllumination(onIlluminatedRunnable: Runnable?) { + isIlluminationRequested = true + animationViewController?.onIlluminationStarting() + + val gView = ghbmView + if (gView != null) { + gView.setGhbmIlluminationListener(this::doIlluminate) + gView.visibility = VISIBLE + gView.startGhbmIllumination(onIlluminatedRunnable) + } else { + doIlluminate(null /* surface */, onIlluminatedRunnable) + } + } + + private fun doIlluminate(surface: Surface?, onIlluminatedRunnable: Runnable?) { + if (ghbmView != null && surface == null) { + Log.e(TAG, "doIlluminate | surface must be non-null for GHBM") + } + + hbmProvider?.enableHbm(hbmType, surface) { + ghbmView?.drawIlluminationDot(sensorRect) + if (onIlluminatedRunnable != null) { + // No framework API can reliably tell when a frame reaches the panel. A timeout + // is the safest solution. + postDelayed(onIlluminatedRunnable, onIlluminatedDelayMs) + } else { + Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null") + } + } + } + + override fun stopIllumination() { + isIlluminationRequested = false + animationViewController?.onIlluminationStopped() + ghbmView?.let { view -> + view.setGhbmIlluminationListener(null) + view.visibility = INVISIBLE + } + hbmProvider?.disableHbm(null /* onHbmDisabled */) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt new file mode 100644 index 0000000000000..57f36172c998f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 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.biometrics + +import android.hardware.biometrics.ComponentInfoInternal +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.biometrics.SensorProperties +import android.hardware.fingerprint.FingerprintSensorProperties +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal + +/** Creates properties from the sensor location with test values. */ +fun SensorLocationInternal.asFingerprintSensorProperties( + sensorId: Int = 22, + @SensorProperties.Strength sensorStrength: Int = SensorProperties.STRENGTH_WEAK, + @FingerprintSensorProperties.SensorType sensorType: Int = + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + maxEnrollmentsPerUser: Int = 1, + info: List = listOf(ComponentInfoInternal("a", "b", "c", "d", "e")), + resetLockoutRequiresHardwareAuthToken: Boolean = false +) = FingerprintSensorPropertiesInternal( + sensorId, + sensorStrength, + maxEnrollmentsPerUser, + info, + sensorType, + resetLockoutRequiresHardwareAuthToken, + listOf(this) +) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 1dea678623b33..8fa2a9a1b8b8f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -165,7 +165,11 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private UdfpsKeyguardView mKeyguardView; @Mock - private UdfpsKeyguardViewController mUdfpsKeyguardViewController; + private UdfpsBpView mBpView; + @Mock + private UdfpsFpmOtherView mFpmOtherView; + private UdfpsAnimationViewController mUdfpsKeyguardViewController = + mock(UdfpsKeyguardViewController.class); @Mock private TypedArray mBrightnessValues; @Mock @@ -192,6 +196,10 @@ public class UdfpsControllerTest extends SysuiTestCase { .thenReturn(mEnrollView); // for showOverlay REASON_ENROLL_ENROLLING when(mLayoutInflater.inflate(R.layout.udfps_keyguard_view, null)) .thenReturn(mKeyguardView); // for showOverlay REASON_AUTH_FPM_KEYGUARD + when(mLayoutInflater.inflate(R.layout.udfps_bp_view, null)) + .thenReturn(mBpView); + when(mLayoutInflater.inflate(R.layout.udfps_fpm_other_view, null)) + .thenReturn(mFpmOtherView); when(mEnrollView.getContext()).thenReturn(mContext); when(mKeyguardStateController.isOccluded()).thenReturn(false); final List props = new ArrayList<>(); @@ -340,7 +348,7 @@ public class UdfpsControllerTest extends SysuiTestCase { when(mKeyguardStateController.canDismissLockScreen()).thenReturn(false); when(mUdfpsView.isWithinSensorArea(anyFloat(), anyFloat())).thenReturn(true); when(mUdfpsView.getAnimationViewController()).thenReturn( - mock(UdfpsEnrollViewController.class)); + (UdfpsAnimationViewController) mock(UdfpsEnrollViewController.class)); // GIVEN that the overlay is showing mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt new file mode 100644 index 0000000000000..2cd470e49d0c0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 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.biometrics + +import android.graphics.PointF +import android.graphics.RectF +import android.hardware.biometrics.SensorLocationInternal +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.ViewUtils +import android.view.LayoutInflater +import android.view.Surface +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.nullable +import org.mockito.Mockito.never +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +private const val DISPLAY_ID = "" // default display id +private const val SENSOR_X = 50 +private const val SENSOR_Y = 250 +private const val SENSOR_RADIUS = 10 + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class UdfpsViewTest : SysuiTestCase() { + + @JvmField @Rule + var rule = MockitoJUnit.rule() + + @Mock + lateinit var hbmProvider: UdfpsHbmProvider + @Mock + lateinit var animationViewController: UdfpsAnimationViewController + + private lateinit var view: UdfpsView + + @Before + fun setup() { + context.setTheme(R.style.Theme_AppCompat) + context.orCreateTestableResources.addOverride( + com.android.internal.R.integer.config_udfps_illumination_transition_ms, 0) + view = LayoutInflater.from(context).inflate(R.layout.udfps_view, null) as UdfpsView + view.animationViewController = animationViewController + view.sensorProperties = + SensorLocationInternal(DISPLAY_ID, SENSOR_X, SENSOR_Y, SENSOR_RADIUS) + .asFingerprintSensorProperties() + view.setHbmProvider(hbmProvider) + ViewUtils.attachView(view) + } + + @After + fun cleanup() { + ViewUtils.detachView(view) + } + + @Test + fun forwardsEvents() { + view.dozeTimeTick() + verify(animationViewController).dozeTimeTick() + + view.onTouchOutsideView() + verify(animationViewController).onTouchOutsideView() + } + + @Test + fun layoutSizeFitsSensor() { + val params = withArgCaptor { + verify(animationViewController).onSensorRectUpdated(capture()) + } + assertThat(params.width()).isAtLeast(2f * SENSOR_RADIUS) + assertThat(params.height()).isAtLeast(2f * SENSOR_RADIUS) + } + + @Test + fun isWithinSensorAreaAndPaused() = isWithinSensorArea(paused = true) + + @Test + fun isWithinSensorAreaAndNotPaused() = isWithinSensorArea(paused = false) + + private fun isWithinSensorArea(paused: Boolean) { + whenever(animationViewController.shouldPauseAuth()).thenReturn(paused) + whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f)) + val end = (SENSOR_RADIUS * 2) - 1 + for (x in 1 until end) { + for (y in 1 until end) { + assertThat(view.isWithinSensorArea(x.toFloat(), y.toFloat())).isEqualTo(!paused) + } + } + } + + @Test + fun isWithinSensorAreaWhenTranslated() { + val offset = PointF(100f, 200f) + whenever(animationViewController.touchTranslation).thenReturn(offset) + val end = (SENSOR_RADIUS * 2) - 1 + for (x in 0 until offset.x.toInt() step 2) { + for (y in 0 until offset.y.toInt() step 2) { + assertThat(view.isWithinSensorArea(x.toFloat(), y.toFloat())).isFalse() + } + } + for (x in offset.x.toInt() + 1 until offset.x.toInt() + end) { + for (y in offset.y.toInt() + 1 until offset.y.toInt() + end) { + assertThat(view.isWithinSensorArea(x.toFloat(), y.toFloat())).isTrue() + } + } + } + + @Test + fun isNotWithinSensorArea() { + whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f)) + assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat())).isFalse() + assertThat(view.isWithinSensorArea(SENSOR_RADIUS.toFloat(), SENSOR_RADIUS * 2.5f)).isFalse() + } + + @Test + fun startAndStopIllumination() { + val onDone: Runnable = mock() + view.startIllumination(onDone) + + val illuminator = withArgCaptor { + verify(hbmProvider).enableHbm(anyInt(), nullable(Surface::class.java), capture()) + } + + assertThat(view.isIlluminationRequested).isTrue() + verify(animationViewController).onIlluminationStarting() + verify(animationViewController, never()).onIlluminationStopped() + verify(onDone, never()).run() + + // fake illumination event + illuminator.run() + waitForLooper() + verify(onDone).run() + verify(hbmProvider, never()).disableHbm(any()) + + view.stopIllumination() + assertThat(view.isIlluminationRequested).isFalse() + verify(animationViewController).onIlluminationStopped() + verify(hbmProvider).disableHbm(nullable(Runnable::class.java)) + } + + private fun waitForLooper() = TestableLooper.get(this).processAllMessages() +} From 0805cb8110cea8c6a63bb7c7330d6b2518bdc856 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Wed, 8 Dec 2021 16:25:40 -0800 Subject: [PATCH 2/3] Split UdfpsController and backfill tests. This splits the monolithic original class into a smaller event handler (original) that delegates UI/request state to a new overlay state holder class, which is migrated to Kotlin. Test: atest UdfpsControllerOverlayTest UdfpsControllerTest Test: manual (enroll manually and BP test app) Bug: 205875955 Change-Id: I8d12a3d130694d2a3fdc1ccb44cf2d30acd92376 --- .../systemui/biometrics/UdfpsController.java | 430 ++++-------------- .../biometrics/UdfpsControllerOverlay.kt | 354 ++++++++++++++ .../biometrics/UdfpsControllerOverlayTest.kt | 288 ++++++++++++ .../biometrics/UdfpsControllerTest.java | 81 +--- 4 files changed, 729 insertions(+), 424 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 1da9f2178b46c..ebf73315c3031 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -24,15 +24,11 @@ import static com.android.systemui.classifier.Classifier.UDFPS_AUTHENTICATION; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.graphics.PixelFormat; -import android.graphics.Point; import android.graphics.RectF; -import android.hardware.biometrics.BiometricOverlayConstants; import android.hardware.biometrics.SensorLocationInternal; import android.hardware.display.DisplayManager; import android.hardware.fingerprint.FingerprintManager; @@ -42,16 +38,13 @@ import android.hardware.fingerprint.IUdfpsOverlayControllerCallback; import android.os.Handler; import android.os.PowerManager; import android.os.Process; -import android.os.RemoteException; import android.os.Trace; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.util.Log; -import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.Surface; import android.view.VelocityTracker; import android.view.View; import android.view.WindowManager; @@ -95,7 +88,7 @@ import kotlin.Unit; * controls/manages all UDFPS sensors. In other words, a single controller is registered with * {@link com.android.server.biometrics.sensors.fingerprint.FingerprintService}, and interfaces such * as {@link FingerprintManager#onPointerDown(int, int, int, float, float)} or - * {@link IUdfpsOverlayController#showUdfpsOverlay(int)} should all have + * {@link IUdfpsOverlayController#showUdfpsOverlay} should all have * {@code sensorId} parameters. */ @SuppressWarnings("deprecation") @@ -136,7 +129,6 @@ public class UdfpsController implements DozeReceiver { // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @VisibleForTesting final FingerprintSensorPropertiesInternal mSensorProps; - private final WindowManager.LayoutParams mCoreLayoutParams; // Tracks the velocity of a touch to help filter out the touches that move too fast. @Nullable private VelocityTracker mVelocityTracker; @@ -150,9 +142,8 @@ public class UdfpsController implements DozeReceiver { // TODO: We should probably try to make touch/illumination things more of a FSM private boolean mGoodCaptureReceived; - @Nullable private UdfpsView mView; // The current request from FingerprintService. Null if no current request. - @Nullable ServerRequest mServerRequest; + @Nullable UdfpsControllerOverlay mOverlay; // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when // to turn off high brightness mode. To get around this limitation, the state of the AOD @@ -164,7 +155,7 @@ public class UdfpsController implements DozeReceiver { private Runnable mAodInterruptRunnable; private boolean mOnFingerDown; private boolean mAttemptedToDismissKeyguard; - private Set mCallbacks = new HashSet<>(); + private final Set mCallbacks = new HashSet<>(); @VisibleForTesting public static final VibrationAttributes VIBRATION_ATTRIBUTES = @@ -193,67 +184,20 @@ public class UdfpsController implements DozeReceiver { } }; - /** - * Keeps track of state within a single FingerprintService request. Note that this state - * persists across configuration changes, etc, since it is considered a single request. - * - * TODO: Perhaps we can move more global variables into here - */ - private static class ServerRequest { - // Reason the overlay has been requested. See IUdfpsOverlayController for definitions. - final int mRequestReason; - @NonNull final IUdfpsOverlayControllerCallback mCallback; - @Nullable final UdfpsEnrollHelper mEnrollHelper; - - ServerRequest(int requestReason, @NonNull IUdfpsOverlayControllerCallback callback, - @Nullable UdfpsEnrollHelper enrollHelper) { - mRequestReason = requestReason; - mCallback = callback; - mEnrollHelper = enrollHelper; - } - - void onEnrollmentProgress(int remaining) { - if (mEnrollHelper != null) { - mEnrollHelper.onEnrollmentProgress(remaining); - } - } - - void onAcquiredGood() { - if (mEnrollHelper != null) { - mEnrollHelper.animateIfLastStep(); - } - } - - void onEnrollmentHelp() { - if (mEnrollHelper != null) { - mEnrollHelper.onEnrollmentHelp(); - } - } - - void onUserCanceled() { - try { - mCallback.onUserCanceled(); - } catch (RemoteException e) { - Log.e(TAG, "Remote exception", e); - } - } - } - public class UdfpsOverlayController extends IUdfpsOverlayController.Stub { @Override public void showUdfpsOverlay(int sensorId, int reason, @NonNull IUdfpsOverlayControllerCallback callback) { - mFgExecutor.execute(() -> { - final UdfpsEnrollHelper enrollHelper; - if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR - || reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING) { - enrollHelper = new UdfpsEnrollHelper(mContext, mFingerprintManager, reason); - } else { - enrollHelper = null; - } - mServerRequest = new ServerRequest(reason, callback, enrollHelper); - updateOverlay(); - }); + mFgExecutor.execute( + () -> UdfpsController.this.showUdfpsOverlay(new UdfpsControllerOverlay( + mContext, mFingerprintManager, mInflater, mWindowManager, + mAccessibilityManager, mStatusBarStateController, + mPanelExpansionStateManager, mKeyguardViewManager, + mKeyguardUpdateMonitor, mDialogManager, mDumpManager, + mLockscreenShadeTransitionController, mConfigurationController, + mSystemClock, mKeyguardStateController, + mUnlockedScreenOffAnimationController, mSensorProps, mHbmProvider, + reason, callback, UdfpsController.this::onTouch))); } @Override @@ -266,57 +210,55 @@ public class UdfpsController implements DozeReceiver { + "mKeyguardUpdateMonitor.isFingerprintDetectionRunning()=true"); } - mServerRequest = null; - updateOverlay(); + UdfpsController.this.hideUdfpsOverlay(); }); } @Override public void onAcquiredGood(int sensorId) { mFgExecutor.execute(() -> { - if (mView == null) { - Log.e(TAG, "Null view when onAcquiredGood for sensorId: " + sensorId); + if (mOverlay == null) { + Log.e(TAG, "Null request when onAcquiredGood for sensorId: " + sensorId); return; } mGoodCaptureReceived = true; - mView.stopIllumination(); - if (mServerRequest != null) { - mServerRequest.onAcquiredGood(); - } else { - Log.e(TAG, "Null serverRequest when onAcquiredGood"); + final UdfpsView view = mOverlay.getOverlayView(); + if (view != null) { + view.stopIllumination(); } + mOverlay.onAcquiredGood(); }); } @Override public void onEnrollmentProgress(int sensorId, int remaining) { mFgExecutor.execute(() -> { - if (mServerRequest == null) { + if (mOverlay == null) { Log.e(TAG, "onEnrollProgress received but serverRequest is null"); return; } - mServerRequest.onEnrollmentProgress(remaining); + mOverlay.onEnrollmentProgress(remaining); }); } @Override public void onEnrollmentHelp(int sensorId) { mFgExecutor.execute(() -> { - if (mServerRequest == null) { + if (mOverlay == null) { Log.e(TAG, "onEnrollmentHelp received but serverRequest is null"); return; } - mServerRequest.onEnrollmentHelp(); + mOverlay.onEnrollmentHelp(); }); } @Override public void setDebugMessage(int sensorId, String message) { mFgExecutor.execute(() -> { - if (mView == null) { + if (mOverlay == null || mOverlay.isHiding()) { return; } - mView.setDebugMessage(message); + mOverlay.getOverlayView().setDebugMessage(message); }); } } @@ -341,14 +283,13 @@ public class UdfpsController implements DozeReceiver { private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (mServerRequest != null - && mServerRequest.mRequestReason != REASON_AUTH_KEYGUARD + if (mOverlay != null + && mOverlay.getRequestReason() != REASON_AUTH_KEYGUARD && Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { Log.d(TAG, "ACTION_CLOSE_SYSTEM_DIALOGS received, mRequestReason: " - + mServerRequest.mRequestReason); - mServerRequest.onUserCanceled(); - mServerRequest = null; - updateOverlay(); + + mOverlay.getRequestReason()); + mOverlay.cancel(); + hideUdfpsOverlay(); } } }; @@ -357,23 +298,12 @@ public class UdfpsController implements DozeReceiver { * Forwards touches to the udfps controller / view */ public boolean onTouch(MotionEvent event) { - if (mView == null) { + if (mOverlay == null || mOverlay.isHiding()) { return false; } - return onTouch(mView, event, false); + return onTouch(mOverlay.getOverlayView(), event, false); } - @SuppressLint("ClickableViewAccessibility") - private final UdfpsView.OnTouchListener mOnTouchListener = (view, event) -> - onTouch(view, event, true); - - @SuppressLint("ClickableViewAccessibility") - private final UdfpsView.OnHoverListener mOnHoverListener = (view, event) -> - onTouch(view, event, true); - - private final AccessibilityManager.TouchExplorationStateChangeListener - mTouchExplorationStateChangeListener = enabled -> updateTouchListener(); - /** * @param x coordinate * @param y coordinate @@ -387,15 +317,15 @@ public class UdfpsController implements DozeReceiver { return udfpsView.isWithinSensorArea(x, y); } - if (mView == null || mView.getAnimationViewController() == null) { + if (mOverlay == null || mOverlay.getAnimationViewController() == null) { return false; } - return !mView.getAnimationViewController().shouldPauseAuth() + return !mOverlay.getAnimationViewController().shouldPauseAuth() && getSensorLocation().contains(x, y); } - private boolean onTouch(View view, MotionEvent event, boolean fromUdfpsView) { + private boolean onTouch(@NonNull View view, @NonNull MotionEvent event, boolean fromUdfpsView) { UdfpsView udfpsView = (UdfpsView) view; final boolean isIlluminationRequested = udfpsView.isIlluminationRequested(); boolean handled = false; @@ -492,7 +422,7 @@ public class UdfpsController implements DozeReceiver { } } else { Log.v(TAG, "onTouch | finger outside"); - onFingerUp(); + onFingerUp(udfpsView); } } Trace.endSection(); @@ -509,7 +439,7 @@ public class UdfpsController implements DozeReceiver { } Log.v(TAG, "onTouch | finger up"); mAttemptedToDismissKeyguard = false; - onFingerUp(); + onFingerUp(udfpsView); mFalsingManager.isFalseTouch(UDFPS_AUTHENTICATION); Trace.endSection(); break; @@ -521,8 +451,8 @@ public class UdfpsController implements DozeReceiver { } private boolean shouldTryToDismissKeyguard() { - return mView.getAnimationViewController() != null - && mView.getAnimationViewController() instanceof UdfpsKeyguardViewController + return mOverlay != null + && mOverlay.getAnimationViewController() instanceof UdfpsKeyguardViewController && mKeyguardStateController.canDismissLockScreen() && !mAttemptedToDismissKeyguard; } @@ -596,17 +526,6 @@ public class UdfpsController implements DozeReceiver { return Unit.INSTANCE; }); - mCoreLayoutParams = new WindowManager.LayoutParams( - WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, - 0 /* flags set in computeLayoutParams() */, - PixelFormat.TRANSLUCENT); - mCoreLayoutParams.setTitle(TAG); - mCoreLayoutParams.setFitInsetsTypes(0); - mCoreLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; - mCoreLayoutParams.layoutInDisplayCutoutMode = - WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; - mCoreLayoutParams.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; - mFingerprintManager.setUdfpsOverlayController(new UdfpsOverlayController()); final IntentFilter filter = new IntentFilter(); @@ -644,8 +563,11 @@ public class UdfpsController implements DozeReceiver { @Override public void dozeTimeTick() { - if (mView != null) { - mView.dozeTimeTick(); + if (mOverlay != null) { + final UdfpsView view = mOverlay.getOverlayView(); + if (view != null) { + view.dozeTimeTick(); + } } } @@ -664,233 +586,63 @@ public class UdfpsController implements DozeReceiver { location.sensorLocationY + location.sensorRadius); } - private void updateOverlay() { - mExecution.assertIsMainThread(); - - if (mServerRequest != null) { - showUdfpsOverlay(mServerRequest); - } else { + private void redrawOverlay() { + UdfpsControllerOverlay overlay = mOverlay; + if (overlay != null) { hideUdfpsOverlay(); + showUdfpsOverlay(overlay); } } - private boolean shouldRotate(@Nullable UdfpsAnimationViewController animation) { - if (!(animation instanceof UdfpsKeyguardViewController)) { - // always rotate view if we're not on the keyguard - return true; - } - - // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded - if (mKeyguardUpdateMonitor.isGoingToSleep() || !mKeyguardStateController.isOccluded()) { - return false; - } - - return true; - } - - private WindowManager.LayoutParams computeLayoutParams( - @Nullable UdfpsAnimationViewController animation) { - final int paddingX = animation != null ? animation.getPaddingX() : 0; - final int paddingY = animation != null ? animation.getPaddingY() : 0; - - mCoreLayoutParams.flags = Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS - | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH; - if (animation != null && animation.listenForTouchesOutsideView()) { - mCoreLayoutParams.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; - } - - // Default dimensions assume portrait mode. - final SensorLocationInternal location = mSensorProps.getLocation(); - mCoreLayoutParams.x = location.sensorLocationX - location.sensorRadius - paddingX; - mCoreLayoutParams.y = location.sensorLocationY - location.sensorRadius - paddingY; - mCoreLayoutParams.height = 2 * location.sensorRadius + 2 * paddingX; - mCoreLayoutParams.width = 2 * location.sensorRadius + 2 * paddingY; - - Point p = new Point(); - // Gets the size based on the current rotation of the display. - mContext.getDisplay().getRealSize(p); - - // Transform dimensions if the device is in landscape mode - switch (mContext.getDisplay().getRotation()) { - case Surface.ROTATION_90: - if (!shouldRotate(animation)) { - Log.v(TAG, "skip rotating udfps location ROTATION_90"); - break; - } else { - Log.v(TAG, "rotate udfps location ROTATION_90"); - } - mCoreLayoutParams.x = location.sensorLocationY - location.sensorRadius - - paddingX; - mCoreLayoutParams.y = p.y - location.sensorLocationX - location.sensorRadius - - paddingY; - break; - - case Surface.ROTATION_270: - if (!shouldRotate(animation)) { - Log.v(TAG, "skip rotating udfps location ROTATION_270"); - break; - } else { - Log.v(TAG, "rotate udfps location ROTATION_270"); - } - mCoreLayoutParams.x = p.x - location.sensorLocationY - location.sensorRadius - - paddingX; - mCoreLayoutParams.y = location.sensorLocationX - location.sensorRadius - - paddingY; - break; - - default: - // Do nothing to stay in portrait mode. - // Keyguard is always in portrait mode. - } - // avoid announcing window title - mCoreLayoutParams.accessibilityTitle = " "; - return mCoreLayoutParams; - } - - private void onOrientationChanged() { // When the configuration changes it's almost always necessary to destroy and re-create // the overlay's window to pass it the new LayoutParams. // Hiding the overlay will destroy its window. It's safe to hide the overlay regardless // of whether it is already hidden. final boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateAuth(); - hideUdfpsOverlay(); // If the overlay needs to be shown, this will re-create and show the overlay with the // updated LayoutParams. Otherwise, the overlay will remain hidden. - updateOverlay(); + redrawOverlay(); if (wasShowingAltAuth) { mKeyguardViewManager.showGenericBouncer(true); } } - private void showUdfpsOverlay(@NonNull ServerRequest request) { + private void showUdfpsOverlay(@NonNull UdfpsControllerOverlay overlay) { mExecution.assertIsMainThread(); - final int reason = request.mRequestReason; - if (mView == null) { - try { - Log.v(TAG, "showUdfpsOverlay | adding window reason=" + reason); - - mView = (UdfpsView) mInflater.inflate(R.layout.udfps_view, null, false); - mOnFingerDown = false; - mView.setSensorProperties(mSensorProps); - mView.setHbmProvider(mHbmProvider); - UdfpsAnimationViewController animation = inflateUdfpsAnimation(reason); - mAttemptedToDismissKeyguard = false; - if (animation != null) { - animation.init(); - mView.setAnimationViewController(animation); - } - mOrientationListener.enable(); - - // This view overlaps the sensor area, so prevent it from being selectable - // during a11y. - if (reason == BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR - || reason == BiometricOverlayConstants.REASON_ENROLL_ENROLLING - || reason == BiometricOverlayConstants.REASON_AUTH_BP) { - mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - } - - mWindowManager.addView(mView, computeLayoutParams(animation)); - mAccessibilityManager.addTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - updateTouchListener(); - } catch (RuntimeException e) { - Log.e(TAG, "showUdfpsOverlay | failed to add window", e); - } + mOverlay = overlay; + if (overlay.show(this)) { + Log.v(TAG, "showUdfpsOverlay | adding window reason=" + + overlay.getRequestReason()); + mOnFingerDown = false; + mAttemptedToDismissKeyguard = false; + mOrientationListener.enable(); } else { Log.v(TAG, "showUdfpsOverlay | the overlay is already showing"); } } - @Nullable - private UdfpsAnimationViewController inflateUdfpsAnimation(int reason) { - switch (reason) { - case BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR: - case BiometricOverlayConstants.REASON_ENROLL_ENROLLING: - UdfpsEnrollView enrollView = (UdfpsEnrollView) mInflater.inflate( - R.layout.udfps_enroll_view, null); - mView.addView(enrollView); - enrollView.updateSensorLocation(mSensorProps); - return new UdfpsEnrollViewController( - enrollView, - mServerRequest.mEnrollHelper, - mStatusBarStateController, - mPanelExpansionStateManager, - mDialogManager, - mDumpManager - ); - case BiometricOverlayConstants.REASON_AUTH_KEYGUARD: - UdfpsKeyguardView keyguardView = (UdfpsKeyguardView) - mInflater.inflate(R.layout.udfps_keyguard_view, null); - mView.addView(keyguardView); - return new UdfpsKeyguardViewController( - keyguardView, - mStatusBarStateController, - mPanelExpansionStateManager, - mKeyguardViewManager, - mKeyguardUpdateMonitor, - mDumpManager, - mLockscreenShadeTransitionController, - mConfigurationController, - mSystemClock, - mKeyguardStateController, - mUnlockedScreenOffAnimationController, - mDialogManager, - this - ); - case BiometricOverlayConstants.REASON_AUTH_BP: - // note: empty controller, currently shows no visual affordance - UdfpsBpView bpView = (UdfpsBpView) mInflater.inflate(R.layout.udfps_bp_view, null); - mView.addView(bpView); - return new UdfpsBpViewController( - bpView, - mStatusBarStateController, - mPanelExpansionStateManager, - mDialogManager, - mDumpManager - ); - case BiometricOverlayConstants.REASON_AUTH_OTHER: - case BiometricOverlayConstants.REASON_AUTH_SETTINGS: - UdfpsFpmOtherView authOtherView = (UdfpsFpmOtherView) - mInflater.inflate(R.layout.udfps_fpm_other_view, null); - mView.addView(authOtherView); - return new UdfpsFpmOtherViewController( - authOtherView, - mStatusBarStateController, - mPanelExpansionStateManager, - mDialogManager, - mDumpManager - ); - default: - Log.e(TAG, "Animation for reason " + reason + " not supported yet"); - return null; - } - } - private void hideUdfpsOverlay() { mExecution.assertIsMainThread(); - if (mView != null) { - Log.v(TAG, "hideUdfpsOverlay | removing window"); + if (mOverlay != null) { // Reset the controller back to its starting state. - onFingerUp(); - boolean wasShowingAltAuth = mKeyguardViewManager.isShowingAlternateAuth(); - mWindowManager.removeView(mView); - mView.setOnTouchListener(null); - mView.setOnHoverListener(null); - mView.setAnimationViewController(null); - if (wasShowingAltAuth) { + final UdfpsView oldView = mOverlay.getOverlayView(); + if (oldView != null) { + onFingerUp(oldView); + } + final boolean removed = mOverlay.hide(); + if (mKeyguardViewManager.isShowingAlternateAuth()) { mKeyguardViewManager.resetAlternateAuth(true); } - mAccessibilityManager.removeTouchExplorationStateChangeListener( - mTouchExplorationStateChangeListener); - mView = null; + Log.v(TAG, "hideUdfpsOverlay | removing window: " + removed); } else { Log.v(TAG, "hideUdfpsOverlay | the overlay is already hidden"); } + mOverlay = null; mOrientationListener.disable(); } @@ -954,7 +706,9 @@ public class UdfpsController implements DozeReceiver { * the user lifts their finger. */ void onCancelUdfps() { - onFingerUp(); + if (mOverlay != null && mOverlay.getOverlayView() != null) { + onFingerUp(mOverlay.getOverlayView()); + } if (!mIsAodInterruptActive) { return; } @@ -971,12 +725,12 @@ public class UdfpsController implements DozeReceiver { private void onFingerDown(int x, int y, float minor, float major) { mExecution.assertIsMainThread(); - if (mView == null) { - Log.w(TAG, "Null view in onFingerDown"); + if (mOverlay == null) { + Log.w(TAG, "Null request in onFingerDown"); return; } - if (mView.getAnimationViewController() instanceof UdfpsKeyguardViewController + if (mOverlay.getAnimationViewController() instanceof UdfpsKeyguardViewController && !mStatusBarStateController.isDozing()) { mKeyguardBypassController.setUserHasDeviceEntryIntent(true); } @@ -991,25 +745,25 @@ public class UdfpsController implements DozeReceiver { mOnFingerDown = true; mFingerprintManager.onPointerDown(mSensorProps.sensorId, x, y, minor, major); Trace.endAsyncSection("UdfpsController.e2e.onPointerDown", 0); - Trace.beginAsyncSection("UdfpsController.e2e.startIllumination", 0); - mView.startIllumination(() -> { - mFingerprintManager.onUiReady(mSensorProps.sensorId); - Trace.endAsyncSection("UdfpsController.e2e.startIllumination", 0); - }); + + final UdfpsView view = mOverlay.getOverlayView(); + if (view != null) { + Trace.beginAsyncSection("UdfpsController.e2e.startIllumination", 0); + view.startIllumination(() -> { + mFingerprintManager.onUiReady(mSensorProps.sensorId); + Trace.endAsyncSection("UdfpsController.e2e.startIllumination", 0); + }); + } for (Callback cb : mCallbacks) { cb.onFingerDown(); } } - private void onFingerUp() { + private void onFingerUp(@NonNull UdfpsView view) { mExecution.assertIsMainThread(); mActivePointerId = -1; mGoodCaptureReceived = false; - if (mView == null) { - Log.w(TAG, "Null view in onFingerUp"); - return; - } if (mOnFingerDown) { mFingerprintManager.onPointerUp(mSensorProps.sensorId); for (Callback cb : mCallbacks) { @@ -1017,22 +771,8 @@ public class UdfpsController implements DozeReceiver { } } mOnFingerDown = false; - if (mView.isIlluminationRequested()) { - mView.stopIllumination(); - } - } - - private void updateTouchListener() { - if (mView == null) { - return; - } - - if (mAccessibilityManager.isTouchExplorationEnabled()) { - mView.setOnHoverListener(mOnHoverListener); - mView.setOnTouchListener(null); - } else { - mView.setOnHoverListener(null); - mView.setOnTouchListener(mOnTouchListener); + if (view.isIlluminationRequested()) { + view.stopIllumination(); } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt new file mode 100644 index 0000000000000..e0912508d1780 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2021 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.biometrics + +import android.annotation.SuppressLint +import android.annotation.UiThread +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Point +import android.hardware.biometrics.BiometricOverlayConstants +import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING +import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricOverlayConstants.ShowReason +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.fingerprint.FingerprintManager +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal +import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.os.RemoteException +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.Surface +import android.view.View +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener +import androidx.annotation.LayoutRes +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.LockscreenShadeTransitionController +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController +import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.time.SystemClock + +private const val TAG = "UdfpsControllerOverlay" + +/** + * Keeps track of the overlay state and UI resources associated with a single FingerprintService + * request. This state can persist across configuration changes via the [show] and [hide] + * methods. + */ +@UiThread +class UdfpsControllerOverlay( + private val context: Context, + fingerprintManager: FingerprintManager, + private val inflater: LayoutInflater, + private val windowManager: WindowManager, + private val accessibilityManager: AccessibilityManager, + private val statusBarStateController: StatusBarStateController, + private val panelExpansionStateManager: PanelExpansionStateManager, + private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val dialogManager: SystemUIDialogManager, + private val dumpManager: DumpManager, + private val transitionController: LockscreenShadeTransitionController, + private val configurationController: ConfigurationController, + private val systemClock: SystemClock, + private val keyguardStateController: KeyguardStateController, + private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, + private val sensorProps: FingerprintSensorPropertiesInternal, + private var hbmProvider: UdfpsHbmProvider, + @ShowReason val requestReason: Int, + private val controllerCallback: IUdfpsOverlayControllerCallback, + private val onTouch: (View, MotionEvent, Boolean) -> Boolean +) { + /** The view, when [isShowing], or null. */ + var overlayView: UdfpsView? = null + private set + + private var overlayTouchListener: TouchExplorationStateChangeListener? = null + + private val coreLayoutParams = WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, + 0 /* flags set in computeLayoutParams() */, + PixelFormat.TRANSLUCENT + ).apply { + title = TAG + fitInsetsTypes = 0 + gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + } + + /** A helper if the [requestReason] was due to enrollment. */ + val enrollHelper: UdfpsEnrollHelper? = if (requestReason.isEnrollmentReason()) { + UdfpsEnrollHelper(context, fingerprintManager, requestReason) + } else { + null + } + + /** If the overlay is currently showing. */ + val isShowing: Boolean + get() = overlayView != null + + /** Opposite of [isShowing]. */ + val isHiding: Boolean + get() = overlayView == null + + /** The animation controller if the overlay [isShowing]. */ + val animationViewController: UdfpsAnimationViewController<*>? + get() = overlayView?.animationViewController + + /** Show the overlay or return false and do nothing if it is already showing. */ + @SuppressLint("ClickableViewAccessibility") + fun show(controller: UdfpsController): Boolean { + if (overlayView == null) { + try { + overlayView = (inflater.inflate( + R.layout.udfps_view, null, false + ) as UdfpsView).apply { + sensorProperties = sensorProps + setHbmProvider(hbmProvider) + val animation = inflateUdfpsAnimation(this, controller) + if (animation != null) { + animation.init() + animationViewController = animation + } + // This view overlaps the sensor area + // prevent it from being selectable during a11y + if (requestReason.isImportantForAccessibility()) { + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + } + + windowManager.addView(this, + coreLayoutParams.updateForLocation(sensorProps.location, animation)) + + overlayTouchListener = TouchExplorationStateChangeListener { + if (accessibilityManager.isTouchExplorationEnabled) { + setOnHoverListener { v, event -> onTouch(v, event, true) } + setOnTouchListener(null) + } else { + setOnHoverListener(null) + setOnTouchListener { v, event -> onTouch(v, event, true) } + } + } + accessibilityManager.addTouchExplorationStateChangeListener( + overlayTouchListener!! + ) + overlayTouchListener?.onTouchExplorationStateChanged(true) + } + } catch (e: RuntimeException) { + Log.e(TAG, "showUdfpsOverlay | failed to add window", e) + } + return true + } + + Log.v(TAG, "showUdfpsOverlay | the overlay is already showing") + return false + } + + private fun inflateUdfpsAnimation( + view: UdfpsView, + controller: UdfpsController + ): UdfpsAnimationViewController<*>? { + return when (requestReason) { + REASON_ENROLL_FIND_SENSOR, + REASON_ENROLL_ENROLLING -> { + UdfpsEnrollViewController( + view.addUdfpsView(R.layout.udfps_enroll_view) { + updateSensorLocation(sensorProps) + }, + enrollHelper ?: throw IllegalStateException("no enrollment helper"), + statusBarStateController, + panelExpansionStateManager, + dialogManager, + dumpManager + ) + } + BiometricOverlayConstants.REASON_AUTH_KEYGUARD -> { + UdfpsKeyguardViewController( + view.addUdfpsView(R.layout.udfps_keyguard_view), + statusBarStateController, + panelExpansionStateManager, + statusBarKeyguardViewManager, + keyguardUpdateMonitor, + dumpManager, + transitionController, + configurationController, + systemClock, + keyguardStateController, + unlockedScreenOffAnimationController, + dialogManager, + controller + ) + } + BiometricOverlayConstants.REASON_AUTH_BP -> { + // note: empty controller, currently shows no visual affordance + UdfpsBpViewController( + view.addUdfpsView(R.layout.udfps_bp_view), + statusBarStateController, + panelExpansionStateManager, + dialogManager, + dumpManager + ) + } + BiometricOverlayConstants.REASON_AUTH_OTHER, + BiometricOverlayConstants.REASON_AUTH_SETTINGS -> { + UdfpsFpmOtherViewController( + view.addUdfpsView(R.layout.udfps_fpm_other_view), + statusBarStateController, + panelExpansionStateManager, + dialogManager, + dumpManager + ) + } + else -> { + Log.e(TAG, "Animation for reason $requestReason not supported yet") + null + } + } + } + + /** Hide the overlay or return false and do nothing if it is already hidden. */ + fun hide(): Boolean { + val wasShowing = isShowing + + overlayView?.apply { + if (isIlluminationRequested) { + stopIllumination() + } + windowManager.removeView(this) + setOnTouchListener(null) + setOnHoverListener(null) + animationViewController = null + overlayTouchListener?.let { + accessibilityManager.removeTouchExplorationStateChangeListener(it) + } + } + overlayView = null + overlayTouchListener = null + + return wasShowing + } + + fun onEnrollmentProgress(remaining: Int) { + enrollHelper?.onEnrollmentProgress(remaining) + } + + fun onAcquiredGood() { + enrollHelper?.animateIfLastStep() + } + + fun onEnrollmentHelp() { + enrollHelper?.onEnrollmentHelp() + } + + /** Cancel this request. */ + fun cancel() { + try { + controllerCallback.onUserCanceled() + } catch (e: RemoteException) { + Log.e(TAG, "Remote exception", e) + } + } + + private fun WindowManager.LayoutParams.updateForLocation( + location: SensorLocationInternal, + animation: UdfpsAnimationViewController<*>? + ): WindowManager.LayoutParams { + val paddingX = animation?.paddingX ?: 0 + val paddingY = animation?.paddingY ?: 0 + flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS + or WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) + if (animation != null && animation.listenForTouchesOutsideView()) { + flags = flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH + } + + // Default dimensions assume portrait mode. + x = location.sensorLocationX - location.sensorRadius - paddingX + y = location.sensorLocationY - location.sensorRadius - paddingY + height = 2 * location.sensorRadius + 2 * paddingX + width = 2 * location.sensorRadius + 2 * paddingY + + // Gets the size based on the current rotation of the display. + val p = Point() + context.display!!.getRealSize(p) + when (context.display!!.rotation) { + Surface.ROTATION_90 -> { + if (!shouldRotate(animation)) { + Log.v(TAG, "skip rotating udfps location ROTATION_90") + } else { + Log.v(TAG, "rotate udfps location ROTATION_90") + x = (location.sensorLocationY - location.sensorRadius - paddingX) + y = (p.y - location.sensorLocationX - location.sensorRadius - paddingY) + } + } + Surface.ROTATION_270 -> { + if (!shouldRotate(animation)) { + Log.v(TAG, "skip rotating udfps location ROTATION_270") + } else { + Log.v(TAG, "rotate udfps location ROTATION_270") + x = (p.x - location.sensorLocationY - location.sensorRadius - paddingX) + y = (location.sensorLocationX - location.sensorRadius - paddingY) + } + } + else -> {} + } + + // avoid announcing window title + accessibilityTitle = " " + + return this + } + + private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean { + if (animation !is UdfpsKeyguardViewController) { + // always rotate view if we're not on the keyguard + return true + } + + // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded + return !(keyguardUpdateMonitor.isGoingToSleep || !keyguardStateController.isOccluded) + } + + private inline fun UdfpsView.addUdfpsView( + @LayoutRes id: Int, + init: T.() -> Unit = {} + ): T { + val subView = inflater.inflate(id, null) as T + addView(subView) + subView.init() + return subView + } +} + +@ShowReason +private fun Int.isEnrollmentReason() = + this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING + +@ShowReason +private fun Int.isImportantForAccessibility() = + this == REASON_ENROLL_FIND_SENSOR || + this == REASON_ENROLL_ENROLLING || + this == BiometricOverlayConstants.REASON_AUTH_BP diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt new file mode 100644 index 0000000000000..84eb93556c86d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2021 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.biometrics + +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER +import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS +import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING +import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR +import android.hardware.biometrics.BiometricOverlayConstants.ShowReason +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.fingerprint.FingerprintManager +import android.hardware.fingerprint.IUdfpsOverlayControllerCallback +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.view.accessibility.AccessibilityManager +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.LockscreenShadeTransitionController +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController +import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.time.SystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +class UdfpsControllerOverlayTest : SysuiTestCase() { + + @JvmField @Rule + var rule = MockitoJUnit.rule() + + @Mock private lateinit var fingerprintManager: FingerprintManager + @Mock private lateinit var inflater: LayoutInflater + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var accessibilityManager: AccessibilityManager + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var panelExpansionStateManager: PanelExpansionStateManager + @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var dialogManager: SystemUIDialogManager + @Mock private lateinit var dumpManager: DumpManager + @Mock private lateinit var transitionController: LockscreenShadeTransitionController + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var systemClock: SystemClock + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock + private lateinit var unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController + @Mock private lateinit var hbmProvider: UdfpsHbmProvider + @Mock private lateinit var controllerCallback: IUdfpsOverlayControllerCallback + @Mock private lateinit var udfpsController: UdfpsController + @Mock private lateinit var udfpsView: UdfpsView + @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView + + private val sensorProps = SensorLocationInternal("", 10, 100, 20) + .asFingerprintSensorProperties() + private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } + private lateinit var controllerOverlay: UdfpsControllerOverlay + + @Before + fun setup() { + context.orCreateTestableResources.addOverride(R.integer.config_udfpsEnrollProgressBar, 20) + whenever(inflater.inflate(R.layout.udfps_view, null, false)) + .thenReturn(udfpsView) + whenever(inflater.inflate(R.layout.udfps_enroll_view, null)) + .thenReturn(udfpsEnrollView) + whenever(inflater.inflate(R.layout.udfps_bp_view, null)) + .thenReturn(mock(UdfpsBpView::class.java)) + whenever(inflater.inflate(R.layout.udfps_keyguard_view, null)) + .thenReturn(mock(UdfpsKeyguardView::class.java)) + whenever(inflater.inflate(R.layout.udfps_fpm_other_view, null)) + .thenReturn(mock(UdfpsFpmOtherView::class.java)) + whenever(udfpsEnrollView.context).thenReturn(context) + } + + private fun withReason(@ShowReason reason: Int, block: () -> Unit) { + controllerOverlay = UdfpsControllerOverlay( + context, fingerprintManager, inflater, windowManager, accessibilityManager, + statusBarStateController, panelExpansionStateManager, statusBarKeyguardViewManager, + keyguardUpdateMonitor, dialogManager, dumpManager, transitionController, + configurationController, systemClock, keyguardStateController, + unlockedScreenOffAnimationController, sensorProps, hbmProvider, reason, + controllerCallback, onTouch) + block() + } + + @Test + fun showUdfpsOverlay_bp() = withReason(REASON_AUTH_BP) { showUdfpsOverlay() } + + @Test + fun showUdfpsOverlay_keyguard() = withReason(REASON_AUTH_KEYGUARD) { showUdfpsOverlay() } + + @Test + fun showUdfpsOverlay_settings() = withReason(REASON_AUTH_SETTINGS) { showUdfpsOverlay() } + + @Test + fun showUdfpsOverlay_locate() = withReason(REASON_ENROLL_FIND_SENSOR) { + showUdfpsOverlay(isEnrollUseCase = true) + } + + @Test + fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { + showUdfpsOverlay(isEnrollUseCase = true) + } + + @Test + fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() } + + private fun showUdfpsOverlay(isEnrollUseCase: Boolean = false) { + val didShow = controllerOverlay.show(udfpsController) + + verify(windowManager).addView(eq(controllerOverlay.overlayView), any()) + verify(udfpsView).setHbmProvider(eq(hbmProvider)) + verify(udfpsView).sensorProperties = eq(sensorProps) + verify(udfpsView).animationViewController = any() + verify(udfpsView).addView(any()) + + assertThat(didShow).isTrue() + assertThat(controllerOverlay.isShowing).isTrue() + assertThat(controllerOverlay.isHiding).isFalse() + assertThat(controllerOverlay.overlayView).isNotNull() + if (isEnrollUseCase) { + verify(udfpsEnrollView).updateSensorLocation(eq(sensorProps)) + assertThat(controllerOverlay.enrollHelper).isNotNull() + } else { + assertThat(controllerOverlay.enrollHelper).isNull() + } + } + + @Test + fun hideUdfpsOverlay_bp() = withReason(REASON_AUTH_BP) { hideUdfpsOverlay() } + + @Test + fun hideUdfpsOverlay_keyguard() = withReason(REASON_AUTH_KEYGUARD) { hideUdfpsOverlay() } + + @Test + fun hideUdfpsOverlay_settings() = withReason(REASON_AUTH_SETTINGS) { hideUdfpsOverlay() } + + @Test + fun hideUdfpsOverlay_locate() = withReason(REASON_ENROLL_FIND_SENSOR) { hideUdfpsOverlay() } + + @Test + fun hideUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { hideUdfpsOverlay() } + + @Test + fun hideUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { hideUdfpsOverlay() } + + private fun hideUdfpsOverlay() { + val didShow = controllerOverlay.show(udfpsController) + val view = controllerOverlay.overlayView + val didHide = controllerOverlay.hide() + + verify(windowManager).removeView(eq(view)) + + assertThat(didShow).isTrue() + assertThat(didHide).isTrue() + assertThat(controllerOverlay.overlayView).isNull() + assertThat(controllerOverlay.animationViewController).isNull() + assertThat(controllerOverlay.isShowing).isFalse() + assertThat(controllerOverlay.isHiding).isTrue() + } + + @Test + fun canNotHide() = withReason(REASON_AUTH_BP) { + assertThat(controllerOverlay.hide()).isFalse() + } + + @Test + fun canNotReshow() = withReason(REASON_AUTH_BP) { + assertThat(controllerOverlay.show(udfpsController)).isTrue() + assertThat(controllerOverlay.show(udfpsController)).isFalse() + } + + @Test + fun forwardEnrollProgressEvents() = withReason(REASON_ENROLL_ENROLLING) { + controllerOverlay.show(udfpsController) + + with(EnrollListener(controllerOverlay)) { + controllerOverlay.onEnrollmentProgress(/* remaining */20) + controllerOverlay.onAcquiredGood() + assertThat(progress).isTrue() + assertThat(help).isFalse() + assertThat(acquired).isFalse() + } + } + + @Test + fun forwardEnrollHelpEvents() = withReason(REASON_ENROLL_ENROLLING) { + controllerOverlay.show(udfpsController) + + with(EnrollListener(controllerOverlay)) { + controllerOverlay.onEnrollmentHelp() + assertThat(progress).isFalse() + assertThat(help).isTrue() + assertThat(acquired).isFalse() + } + } + + @Test + fun forwardEnrollAcquiredEvents() = withReason(REASON_ENROLL_ENROLLING) { + controllerOverlay.show(udfpsController) + + with(EnrollListener(controllerOverlay)) { + controllerOverlay.onEnrollmentProgress(/* remaining */ 1) + controllerOverlay.onAcquiredGood() + assertThat(progress).isTrue() + assertThat(help).isFalse() + assertThat(acquired).isTrue() + } + } + + @Test + fun cancels() = withReason(REASON_AUTH_BP) { + controllerOverlay.cancel() + verify(controllerCallback).onUserCanceled() + } + + @Test + fun stopIlluminatingOnHide() = withReason(REASON_AUTH_BP) { + whenever(udfpsView.isIlluminationRequested).thenReturn(true) + + controllerOverlay.show(udfpsController) + controllerOverlay.hide() + verify(udfpsView).stopIllumination() + } +} + +private class EnrollListener( + overlay: UdfpsControllerOverlay, + var progress: Boolean = false, + var help: Boolean = false, + var acquired: Boolean = false +) : UdfpsEnrollHelper.Listener { + + init { + overlay.enrollHelper!!.setListener(this) + } + + override fun onEnrollmentProgress(remaining: Int, totalSteps: Int) { + progress = true + } + + override fun onEnrollmentHelp(remaining: Int, totalSteps: Int) { + help = true + } + + override fun onLastStepAcquired() { + acquired = true + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 8fa2a9a1b8b8f..d5725786e0d27 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -163,11 +163,11 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private UdfpsEnrollView mEnrollView; @Mock - private UdfpsKeyguardView mKeyguardView; - @Mock private UdfpsBpView mBpView; @Mock private UdfpsFpmOtherView mFpmOtherView; + @Mock + private UdfpsKeyguardView mKeyguardView; private UdfpsAnimationViewController mUdfpsKeyguardViewController = mock(UdfpsKeyguardViewController.class); @Mock @@ -413,83 +413,6 @@ public class UdfpsControllerTest extends SysuiTestCase { verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); } - @Test - public void showUdfpsOverlay_addsViewToWindow_bp() throws RemoteException { - showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_BP); - } - - @Test - public void showUdfpsOverlay_addsViewToWindow_keyguard() throws RemoteException { - showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_KEYGUARD); - } - - @Test - public void showUdfpsOverlay_addsViewToWindow_settings() throws RemoteException { - showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_SETTINGS); - } - - @Test - public void showUdfpsOverlay_addsViewToWindow_enroll_locate() throws RemoteException { - showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR); - } - - @Test - public void showUdfpsOverlay_addsViewToWindow_enroll() throws RemoteException { - showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_ENROLL_ENROLLING); - } - - @Test - public void showUdfpsOverlay_addsViewToWindow_other() throws RemoteException { - showUdfpsOverlay_addsViewToWindow(BiometricOverlayConstants.REASON_AUTH_OTHER); - } - - private void showUdfpsOverlay_addsViewToWindow( - @BiometricOverlayConstants.ShowReason int reason) throws RemoteException { - mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, reason, - mUdfpsOverlayControllerCallback); - mFgExecutor.runAllReady(); - verify(mWindowManager).addView(eq(mUdfpsView), any()); - } - - @Test - public void hideUdfpsOverlay_removesViewFromWindow_bp() throws RemoteException { - hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_BP); - } - - @Test - public void hideUdfpsOverlay_removesViewFromWindow_keyguard() throws RemoteException { - hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_KEYGUARD); - } - - @Test - public void hideUdfpsOverlay_removesViewFromWindow_settings() throws RemoteException { - hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_SETTINGS); - } - - @Test - public void hideUdfpsOverlay_removesViewFromWindow_enroll_locate() throws RemoteException { - hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR); - } - - @Test - public void hideUdfpsOverlay_removesViewFromWindow_enroll() throws RemoteException { - hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_ENROLL_ENROLLING); - } - - @Test - public void hideUdfpsOverlay_removesViewFromWindow_other() throws RemoteException { - hideUdfpsOverlay_removesViewFromWindow(BiometricOverlayConstants.REASON_AUTH_OTHER); - } - - private void hideUdfpsOverlay_removesViewFromWindow( - @BiometricOverlayConstants.ShowReason int reason) throws RemoteException { - mOverlayController.showUdfpsOverlay(TEST_UDFPS_SENSOR_ID, - BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); - mOverlayController.hideUdfpsOverlay(TEST_UDFPS_SENSOR_ID); - mFgExecutor.runAllReady(); - verify(mWindowManager).removeView(eq(mUdfpsView)); - } - @Test public void hideUdfpsOverlay_resetsAltAuthBouncerWhenShowing() throws RemoteException { // GIVEN overlay was showing and the udfps bouncer is showing From b1c14109a913d072e38685dbcbd9d9b61a6a4f93 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Tue, 7 Dec 2021 13:39:43 -0800 Subject: [PATCH 3/3] Instrument UDFPS touch affordance CUJ. Bug: 205875955 Test: atest UdfpsControllerTest Change-Id: I735227bb2453d7c93d7d0350a32ea1f02f54d3a7 --- .../android/internal/util/LatencyTracker.java | 16 +++++++++++++--- .../systemui/biometrics/UdfpsController.java | 9 +++++++-- .../biometrics/UdfpsFpmOtherViewController.kt | 1 - .../systemui/biometrics/UdfpsControllerTest.java | 15 ++++++++++++--- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java index e95ba884fed4f..1cd758c0f43b1 100644 --- a/core/java/com/android/internal/util/LatencyTracker.java +++ b/core/java/com/android/internal/util/LatencyTracker.java @@ -127,6 +127,11 @@ public class LatencyTracker { */ public static final int ACTION_SWITCH_DISPLAY_UNFOLD = 13; + /** + * Time it takes for a UDFPS sensor to appear ready after it is touched. + */ + public static final int ACTION_UDFPS_ILLUMINATE = 14; + private static final int[] ACTIONS_ALL = { ACTION_EXPAND_PANEL, ACTION_TOGGLE_RECENTS, @@ -141,7 +146,8 @@ public class LatencyTracker { ACTION_ROTATE_SCREEN_CAMERA_CHECK, ACTION_LOCKSCREEN_UNLOCK, ACTION_USER_SWITCH, - ACTION_SWITCH_DISPLAY_UNFOLD + ACTION_SWITCH_DISPLAY_UNFOLD, + ACTION_UDFPS_ILLUMINATE }; /** @hide */ @@ -159,7 +165,8 @@ public class LatencyTracker { ACTION_ROTATE_SCREEN_CAMERA_CHECK, ACTION_LOCKSCREEN_UNLOCK, ACTION_USER_SWITCH, - ACTION_SWITCH_DISPLAY_UNFOLD + ACTION_SWITCH_DISPLAY_UNFOLD, + ACTION_UDFPS_ILLUMINATE }) @Retention(RetentionPolicy.SOURCE) public @interface Action { @@ -179,7 +186,8 @@ public class LatencyTracker { FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK, FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK, FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH, - FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD + FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD, + FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_UDFPS_ILLUMINATE }; private static LatencyTracker sLatencyTracker; @@ -267,6 +275,8 @@ public class LatencyTracker { return "ACTION_USER_SWITCH"; case 14: return "ACTION_SWITCH_DISPLAY_UNFOLD"; + case 15: + return "ACTION_UDFPS_ILLUMINATE"; default: throw new IllegalArgumentException("Invalid action"); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index ebf73315c3031..fd7ae323e88f1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -51,8 +51,8 @@ import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.DozeReceiver; @@ -125,6 +125,7 @@ public class UdfpsController implements DozeReceiver { @NonNull private final SystemClock mSystemClock; @NonNull private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; + @NonNull private final LatencyTracker mLatencyTracker; @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener; // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple // sensors, this, in addition to a lot of the code here, will be updated. @@ -349,6 +350,7 @@ public class UdfpsController implements DozeReceiver { boolean withinSensorArea = isWithinSensorArea(udfpsView, event.getX(), event.getY(), fromUdfpsView); if (withinSensorArea) { + mLatencyTracker.onActionStart(LatencyTracker.ACTION_UDFPS_ILLUMINATE); Trace.beginAsyncSection("UdfpsController.e2e.onPointerDown", 0); Log.v(TAG, "onTouch | action down"); // The pointer that causes ACTION_DOWN is always at index 0. @@ -484,7 +486,8 @@ public class UdfpsController implements DozeReceiver { @NonNull ConfigurationController configurationController, @NonNull SystemClock systemClock, @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, - @NonNull SystemUIDialogManager dialogManager) { + @NonNull SystemUIDialogManager dialogManager, + @NonNull LatencyTracker latencyTracker) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -512,6 +515,7 @@ public class UdfpsController implements DozeReceiver { mConfigurationController = configurationController; mSystemClock = systemClock; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; + mLatencyTracker = latencyTracker; mSensorProps = findFirstUdfps(); // At least one UDFPS sensor exists @@ -751,6 +755,7 @@ public class UdfpsController implements DozeReceiver { Trace.beginAsyncSection("UdfpsController.e2e.startIllumination", 0); view.startIllumination(() -> { mFingerprintManager.onUiReady(mSensorProps.sensorId); + mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE); Trace.endAsyncSection("UdfpsController.e2e.startIllumination", 0); }); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt index f56b5d8af4717..98205cfb7966e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmOtherViewController.kt @@ -15,7 +15,6 @@ */ package com.android.systemui.biometrics - import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.SystemUIDialogManager diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index d5725786e0d27..2afcbda64fb57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -24,6 +24,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -53,6 +54,7 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; +import com.android.internal.util.LatencyTracker; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; @@ -81,6 +83,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -154,7 +157,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private SystemClock mSystemClock; @Mock private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; - + @Mock + private LatencyTracker mLatencyTracker; private FakeExecutor mFgExecutor; // Stuff for configuring mocks @@ -247,7 +251,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mConfigurationController, mSystemClock, mUnlockedScreenOffAnimationController, - mSystemUIDialogManager); + mSystemUIDialogManager, + mLatencyTracker); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture()); @@ -463,11 +468,15 @@ public class UdfpsControllerTest extends SysuiTestCase { // THEN FingerprintManager is notified about onPointerDown verify(mFingerprintManager).onPointerDown(eq(mUdfpsController.mSensorProps.sensorId), eq(0), eq(0), eq(0f), eq(0f)); + verify(mLatencyTracker).onActionStart(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); // AND illumination begins verify(mUdfpsView).startIllumination(mOnIlluminatedRunnableCaptor.capture()); + verify(mLatencyTracker, never()).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); // AND onIlluminatedRunnable notifies FingerprintManager about onUiReady mOnIlluminatedRunnableCaptor.getValue().run(); - verify(mFingerprintManager).onUiReady(eq(mUdfpsController.mSensorProps.sensorId)); + InOrder inOrder = inOrder(mFingerprintManager, mLatencyTracker); + inOrder.verify(mFingerprintManager).onUiReady(eq(mUdfpsController.mSensorProps.sensorId)); + inOrder.verify(mLatencyTracker).onActionEnd(eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); } @Test