From 05c83b715a463388d45f2c0aaff5b4225541e1c4 Mon Sep 17 00:00:00 2001 From: Joe Bolinger Date: Mon, 29 Nov 2021 11:58:48 -0800 Subject: [PATCH] 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() +}