From d1ae004bf0545c8527c8f4f2343bf08214034931 Mon Sep 17 00:00:00 2001 From: Curtis Belmonte Date: Mon, 22 Mar 2021 15:15:13 -0700 Subject: [PATCH] Improve BiometricPrompt support for face + UDFPS On devices with an available face and UDFPS sensor, show a BiometricPrompt that first attempts to authenticate with face, falling back to UDFPS if unsuccessful. This change is UI-only for now, and follow-up work will be needed to coordinate fallback behavior between sensors, rather than running both simultaneously. Test: atest com.android.systemui.biometrics Test: Manually tested BiometricPrompt on device Bug: 172376593 Bug: 183220060 Change-Id: Ia128e97d2d831d14420ec015f32eaa589b8c38cd --- .../res/layout/auth_biometric_contents.xml | 4 +- .../auth_biometric_face_to_udfps_view.xml | 25 ++ .../AuthBiometricFaceToUdfpsView.java | 147 ++++++++ .../biometrics/AuthBiometricFaceView.java | 55 +-- .../biometrics/AuthBiometricUdfpsView.java | 284 +-------------- .../biometrics/AuthBiometricView.java | 17 +- .../biometrics/AuthContainerView.java | 55 ++- .../biometrics/UdfpsDialogMeasureAdapter.java | 330 ++++++++++++++++++ .../biometrics/AuthBiometricViewTest.java | 87 ----- .../UdfpsDialogMeasureAdapterTest.java | 119 +++++++ 10 files changed, 727 insertions(+), 396 deletions(-) create mode 100644 packages/SystemUI/res/layout/auth_biometric_face_to_udfps_view.xml create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java create mode 100644 packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml index eef37ab20c693..99e8116cb6818 100644 --- a/packages/SystemUI/res/layout/auth_biometric_contents.xml +++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml @@ -44,7 +44,7 @@ + android:paddingTop="24dp"> + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java new file mode 100644 index 0000000000000..197f35ba3e86b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceToUdfpsView.java @@ -0,0 +1,147 @@ +/* + * 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 static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.systemui.R; + +/** + * Manages the layout of an auth dialog for devices with a face sensor and an under-display + * fingerprint sensor (UDFPS). Face authentication is attempted first, followed by fingerprint if + * the initial attempt is unsuccessful. + */ +public class AuthBiometricFaceToUdfpsView extends AuthBiometricFaceView { + private static final String TAG = "BiometricPrompt/AuthBiometricFaceToUdfpsView"; + + protected static class UdfpsIconController extends IconController { + protected UdfpsIconController( + @NonNull Context context, @NonNull ImageView iconView, @NonNull TextView textView) { + super(context, iconView, textView); + } + + @Override + protected void updateState(int lastState, int newState) { + final boolean lastStateIsErrorIcon = + lastState == STATE_ERROR || lastState == STATE_HELP; + + switch (newState) { + case STATE_IDLE: + case STATE_AUTHENTICATING_ANIMATING_IN: + case STATE_AUTHENTICATING: + case STATE_PENDING_CONFIRMATION: + case STATE_AUTHENTICATED: + if (lastStateIsErrorIcon) { + animateOnce(R.drawable.fingerprint_dialog_error_to_fp); + } else { + showStaticDrawable(R.drawable.fingerprint_dialog_fp_to_error); + } + mIconView.setContentDescription(mContext.getString( + R.string.accessibility_fingerprint_dialog_fingerprint_icon)); + break; + + case STATE_ERROR: + case STATE_HELP: + if (!lastStateIsErrorIcon) { + animateOnce(R.drawable.fingerprint_dialog_fp_to_error); + } else { + showStaticDrawable(R.drawable.fingerprint_dialog_error_to_fp); + } + mIconView.setContentDescription(mContext.getString( + R.string.biometric_dialog_try_again)); + break; + + default: + Log.e(TAG, "Unknown biometric dialog state: " + newState); + break; + } + + mState = newState; + } + } + + @BiometricAuthenticator.Modality private int mActiveSensorType = TYPE_FACE; + + @Nullable UdfpsDialogMeasureAdapter mMeasureAdapter; + @Nullable private UdfpsIconController mUdfpsIconController; + + public AuthBiometricFaceToUdfpsView(Context context) { + super(context); + } + + public AuthBiometricFaceToUdfpsView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { + if (mMeasureAdapter == null || mMeasureAdapter.getSensorProps() != sensorProps) { + mMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps); + } + } + + @Override + protected int getDelayAfterAuthenticatedDurationMs() { + return mActiveSensorType == TYPE_FINGERPRINT ? 0 + : super.getDelayAfterAuthenticatedDurationMs(); + } + + @Override + protected boolean supportsManualRetry() { + return false; + } + + @Override + @NonNull + protected IconController getIconController() { + if (mActiveSensorType == TYPE_FINGERPRINT) { + if (!(mIconController instanceof UdfpsIconController)) { + mIconController = new UdfpsIconController(getContext(), mIconView, mIndicatorView); + } + return mIconController; + } + return super.getIconController(); + } + + @Override + public void updateState(int newState) { + if (mState == STATE_HELP || mState == STATE_ERROR) { + mActiveSensorType = TYPE_FINGERPRINT; + setRequireConfirmation(false); + } + super.updateState(newState); + } + + @Override + @NonNull + AuthDialog.LayoutParams onMeasureInternal(int width, int height) { + final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); + return mMeasureAdapter != null + ? mMeasureAdapter.onMeasureInternal(width, height, layoutParams) + : layoutParams; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java index 9b09cfd0dba69..4c5ca691939c4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricFaceView.java @@ -16,6 +16,7 @@ package com.android.systemui.biometrics; +import android.annotation.NonNull; import android.content.Context; import android.graphics.drawable.Animatable2; import android.graphics.drawable.AnimatedVectorDrawable; @@ -28,7 +29,6 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; -import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.R; public class AuthBiometricFaceView extends AuthBiometricView { @@ -38,15 +38,15 @@ public class AuthBiometricFaceView extends AuthBiometricView { // Delay before dismissing after being authenticated/confirmed. private static final int HIDE_DELAY_MS = 500; - public static class IconController extends Animatable2.AnimationCallback { - Context mContext; - ImageView mIconView; - TextView mTextView; - Handler mHandler; - boolean mLastPulseLightToDark; // false = dark to light, true = light to dark - @BiometricState int mState; + protected static class IconController extends Animatable2.AnimationCallback { + protected Context mContext; + protected ImageView mIconView; + protected TextView mTextView; + protected Handler mHandler; + protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark + protected @BiometricState int mState; - IconController(Context context, ImageView iconView, TextView textView) { + protected IconController(Context context, ImageView iconView, TextView textView) { mContext = context; mIconView = iconView; mTextView = textView; @@ -54,15 +54,15 @@ public class AuthBiometricFaceView extends AuthBiometricView { showStaticDrawable(R.drawable.face_dialog_pulse_dark_to_light); } - void animateOnce(int iconRes) { + protected void animateOnce(int iconRes) { animateIcon(iconRes, false); } - public void showStaticDrawable(int iconRes) { + protected void showStaticDrawable(int iconRes) { mIconView.setImageDrawable(mContext.getDrawable(iconRes)); } - void animateIcon(int iconRes, boolean repeat) { + protected void animateIcon(int iconRes, boolean repeat) { final AnimatedVectorDrawable icon = (AnimatedVectorDrawable) mContext.getDrawable(iconRes); mIconView.setImageDrawable(icon); @@ -73,12 +73,12 @@ public class AuthBiometricFaceView extends AuthBiometricView { icon.start(); } - void startPulsing() { + protected void startPulsing() { mLastPulseLightToDark = false; animateIcon(R.drawable.face_dialog_pulse_dark_to_light, true); } - void pulseInNextDirection() { + protected void pulseInNextDirection() { int iconRes = mLastPulseLightToDark ? R.drawable.face_dialog_pulse_dark_to_light : R.drawable.face_dialog_pulse_light_to_dark; animateIcon(iconRes, true /* repeat */); @@ -93,7 +93,7 @@ public class AuthBiometricFaceView extends AuthBiometricView { } } - public void updateState(int lastState, int newState) { + protected void updateState(int lastState, int newState) { final boolean lastStateIsErrorIcon = lastState == STATE_ERROR || lastState == STATE_HELP; @@ -138,7 +138,7 @@ public class AuthBiometricFaceView extends AuthBiometricView { } } - @VisibleForTesting IconController mIconController; + protected IconController mIconController; public AuthBiometricFaceView(Context context) { this(context, null); @@ -174,14 +174,21 @@ public class AuthBiometricFaceView extends AuthBiometricView { } @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mIconController = new IconController(mContext, mIconView, mIndicatorView); + protected boolean supportsManualRetry() { + return true; + } + + @NonNull + protected IconController getIconController() { + if (mIconController == null) { + mIconController = new IconController(mContext, mIconView, mIndicatorView); + } + return mIconController; } @Override public void updateState(@BiometricState int newState) { - mIconController.updateState(mState, newState); + getIconController().updateState(mState, newState); if (newState == STATE_AUTHENTICATING_ANIMATING_IN || (newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) { @@ -195,11 +202,13 @@ public class AuthBiometricFaceView extends AuthBiometricView { @Override public void onAuthenticationFailed(String failureReason) { if (getSize() == AuthDialog.SIZE_MEDIUM) { - mTryAgainButton.setVisibility(View.VISIBLE); - mConfirmButton.setVisibility(View.GONE); + if (supportsManualRetry()) { + mTryAgainButton.setVisibility(View.VISIBLE); + mConfirmButton.setVisibility(View.GONE); + } } - // Do this last since wa want to know if the button is being animated (in the case of + // Do this last since we want to know if the button is being animated (in the case of // small -> medium dialog) super.onAuthenticationFailed(failureReason); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java index 007080bc8603f..376368fbf9d4e 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricUdfpsView.java @@ -16,33 +16,18 @@ package com.android.systemui.biometrics; -import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; -import android.graphics.Insets; -import android.graphics.Rect; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.util.AttributeSet; -import android.util.Log; -import android.view.Surface; -import android.view.View; -import android.view.WindowInsets; -import android.view.WindowManager; -import android.widget.FrameLayout; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.systemui.R; /** * Manages the layout for under-display fingerprint sensors (UDFPS). Ensures that UI elements * do not overlap with */ public class AuthBiometricUdfpsView extends AuthBiometricFingerprintView { - - private static final String TAG = "AuthBiometricUdfpsView"; - - @Nullable private FingerprintSensorPropertiesInternal mSensorProps; + @Nullable private UdfpsDialogMeasureAdapter mMeasureAdapter; public AuthBiometricUdfpsView(Context context) { this(context, null /* attrs */); @@ -52,269 +37,18 @@ public class AuthBiometricUdfpsView extends AuthBiometricFingerprintView { super(context, attrs); } - void setSensorProps(@NonNull FingerprintSensorPropertiesInternal prop) { - mSensorProps = prop; + void setSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) { + if (mMeasureAdapter == null || mMeasureAdapter.getSensorProps() != sensorProps) { + mMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps); + } } @Override @NonNull AuthDialog.LayoutParams onMeasureInternal(int width, int height) { - final int displayRotation = getDisplay().getRotation(); - switch (displayRotation) { - case Surface.ROTATION_0: - return onMeasureInternalPortrait(width, height); - case Surface.ROTATION_90: - case Surface.ROTATION_270: - return onMeasureInternalLandscape(width, height); - default: - Log.e(TAG, "Unsupported display rotation: " + displayRotation); - return super.onMeasureInternal(width, height); - } - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height) { - // Get the height of the everything below the icon. Currently, that's the indicator and - // button bar. - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - - // Figure out where the bottom of the sensor anim should be. - // Navbar + dialogMargin + buttonBar + textIndicator + spacerHeight = sensorDistFromBottom - final int dialogMargin = getDialogMarginPx(); - final WindowManager windowManager = getContext().getSystemService(WindowManager.class); - final int displayHeight = getWindowBounds(windowManager).height(); - final Insets navbarInsets = getNavbarInsets(windowManager); - final int bottomSpacerHeight = calculateBottomSpacerHeightForPortrait( - mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, - dialogMargin, navbarInsets.bottom); - - // Go through each of the children and do the custom measurement. - int totalHeight = 0; - final int numChildren = getChildCount(); - final int sensorDiameter = mSensorProps.sensorRadius * 2; - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_above_icon) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Set the spacer height so the fingerprint icon is on the physical sensor area - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); - } else { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - totalHeight += child.getMeasuredHeight(); - } - } - - return new AuthDialog.LayoutParams(width, totalHeight); - } - - @NonNull - private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height) { - // Find the spacer height needed to vertically align the icon with the sensor. - final int titleHeight = getViewHeightPx(R.id.title); - final int subtitleHeight = getViewHeightPx(R.id.subtitle); - final int descriptionHeight = getViewHeightPx(R.id.description); - final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); - final int textIndicatorHeight = getViewHeightPx(R.id.indicator); - final int buttonBarHeight = getViewHeightPx(R.id.button_bar); - final WindowManager windowManager = getContext().getSystemService(WindowManager.class); - final Insets navbarInsets = getNavbarInsets(windowManager); - final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, - subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, - buttonBarHeight, navbarInsets.bottom); - - // Find the spacer width needed to horizontally align the icon with the sensor. - final int displayWidth = getWindowBounds(windowManager).width(); - final int dialogMargin = getDialogMarginPx(); - final int horizontalInset = navbarInsets.left + navbarInsets.right; - final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( - mSensorProps, displayWidth, dialogMargin, horizontalInset); - - final int sensorDiameter = mSensorProps.sensorRadius * 2; - final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; - - int remeasuredHeight = 0; - final int numChildren = getChildCount(); - for (int i = 0; i < numChildren; i++) { - final View child = getChildAt(i); - if (child.getId() == R.id.biometric_icon_frame) { - final FrameLayout iconFrame = (FrameLayout) child; - final View icon = iconFrame.getChildAt(0); - - // Ensure that the icon is never larger than the sensor. - icon.measure( - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); - - // Create a frame that's exactly the height of the sensor circle. - iconFrame.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_above_icon || child.getId() == R.id.button_bar) { - // Adjust the width of the top spacer and button bar while preserving their heights. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec( - child.getLayoutParams().height, MeasureSpec.EXACTLY)); - } else if (child.getId() == R.id.space_below_icon) { - // Adjust the bottom spacer height to align the fingerprint icon with the sensor. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); - } else { - // Use the remeasured width for all other child views. - child.measure( - MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } - - if (child.getVisibility() != View.GONE) { - remeasuredHeight += child.getMeasuredHeight(); - } - } - - return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); - } - - private int getViewHeightPx(@IdRes int viewId) { - final View view = findViewById(viewId); - return view != null ? view.getMeasuredHeight() : 0; - } - - private int getDialogMarginPx() { - return getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); - } - - @NonNull - private static Insets getNavbarInsets(@Nullable WindowManager windowManager) { - return windowManager != null && windowManager.getCurrentWindowMetrics() != null - ? windowManager.getCurrentWindowMetrics().getWindowInsets() - .getInsets(WindowInsets.Type.navigationBars()) - : Insets.NONE; - } - - @NonNull - private static Rect getWindowBounds(@Nullable WindowManager windowManager) { - return windowManager != null && windowManager.getCurrentWindowMetrics() != null - ? windowManager.getCurrentWindowMetrics().getBounds() - : new Rect(); - } - - /** - * For devices in portrait orientation where the sensor is too high up, calculates the amount of - * padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForPortrait( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, - int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, - int navbarBottomInsetPx) { - - final int sensorDistanceFromBottom = displayHeightPx - - sensorProperties.sensorLocationY - - sensorProperties.sensorRadius; - - final int spacerHeight = sensorDistanceFromBottom - - textIndicatorHeightPx - - buttonBarHeightPx - - dialogMarginPx - - navbarBottomInsetPx; - - Log.d(TAG, "Display height: " + displayHeightPx - + ", Distance from bottom: " + sensorDistanceFromBottom - + ", Bottom margin: " + dialogMarginPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (portrait): " + spacerHeight); - - return spacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too high up, calculates the amount - * of padding necessary to center the biometric icon within the sensor's physical location. - */ - @VisibleForTesting - static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, - int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, - int buttonBarHeightPx, int navbarBottomInsetPx) { - - final int dialogHeightAboveIcon = titleHeightPx - + subtitleHeightPx - + descriptionHeightPx - + topSpacerHeightPx; - - final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; - - final int bottomSpacerHeight = dialogHeightAboveIcon - - dialogHeightBelowIcon - - navbarBottomInsetPx; - - Log.d(TAG, "Title height: " + titleHeightPx - + ", Subtitle height: " + subtitleHeightPx - + ", Description height: " + descriptionHeightPx - + ", Top spacer height: " + topSpacerHeightPx - + ", Text indicator height: " + textIndicatorHeightPx - + ", Button bar height: " + buttonBarHeightPx - + ", Navbar bottom inset: " + navbarBottomInsetPx - + ", Bottom spacer height (landscape): " + bottomSpacerHeight); - - return bottomSpacerHeight; - } - - /** - * For devices in landscape orientation where the sensor is too left/right, calculates the - * amount of padding necessary to center the biometric icon within the sensor's physical - * location. - */ - @VisibleForTesting - static int calculateHorizontalSpacerWidthForLandscape( - @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, - int dialogMarginPx, int navbarHorizontalInsetPx) { - - final int sensorDistanceFromEdge = displayWidthPx - - sensorProperties.sensorLocationY - - sensorProperties.sensorRadius; - - final int horizontalPadding = sensorDistanceFromEdge - - dialogMarginPx - - navbarHorizontalInsetPx; - - Log.d(TAG, "Display width: " + displayWidthPx - + ", Distance from edge: " + sensorDistanceFromEdge - + ", Dialog margin: " + dialogMarginPx - + ", Navbar horizontal inset: " + navbarHorizontalInsetPx - + ", Horizontal spacer width (landscape): " + horizontalPadding); - - return horizontalPadding; + final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height); + return mMeasureAdapter != null + ? mMeasureAdapter.onMeasureInternal(width, height, layoutParams) + : layoutParams; } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index d59a865e2adda..a40af70adb8f5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -379,7 +379,9 @@ public abstract class AuthBiometricView extends LinearLayout { } else { mNegativeButton.setVisibility(View.VISIBLE); } - mTryAgainButton.setVisibility(View.VISIBLE); + if (supportsManualRetry()) { + mTryAgainButton.setVisibility(View.VISIBLE); + } if (!TextUtils.isEmpty(mSubtitleView.getText())) { mSubtitleView.setVisibility(View.VISIBLE); @@ -462,6 +464,10 @@ public abstract class AuthBiometricView extends LinearLayout { Utils.notifyAccessibilityContentChanged(mAccessibilityManager, this); } + protected boolean supportsManualRetry() { + return false; + } + public void updateState(@BiometricState int newState) { Log.v(TAG, "newState: " + newState); @@ -749,7 +755,9 @@ public abstract class AuthBiometricView extends LinearLayout { for (int i = 0; i < numChildren; i++) { final View child = getChildAt(i); - if (child.getId() == R.id.space_above_icon) { + if (child.getId() == R.id.space_above_icon + || child.getId() == R.id.space_below_icon + || child.getId() == R.id.button_bar) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, @@ -765,11 +773,6 @@ public abstract class AuthBiometricView extends LinearLayout { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); - } else if (child.getId() == R.id.button_bar) { - child.measure( - MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, - MeasureSpec.EXACTLY)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index d05e9278762dd..4e93f58dd51f5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -282,8 +282,9 @@ public class AuthContainerView extends LinearLayout mPanelController = mInjector.getPanelController(mContext, mPanelView); // Inflate biometric view only if necessary. + final int sensorCount = config.mSensorIds.length; if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) { - if (config.mSensorIds.length == 1 || config.mSensorIds.length == 2) { + if (sensorCount == 1) { final int singleSensorAuthId = config.mSensorIds[0]; if (Utils.containsSensorId(mFpProps, singleSensorAuthId)) { FingerprintSensorPropertiesInternal sensorProps = null; @@ -314,8 +315,54 @@ public class AuthContainerView extends LinearLayout mBiometricScrollView = null; return; } + } else if (sensorCount == 2) { + int fingerprintSensorId = -1; + int faceSensorId = -1; + for (final int sensorId : config.mSensorIds) { + if (Utils.containsSensorId(mFpProps, sensorId)) { + fingerprintSensorId = sensorId; + continue; + } else if (Utils.containsSensorId(mFaceProps, sensorId)) { + faceSensorId = sensorId; + continue; + } + + if (fingerprintSensorId != -1 && faceSensorId != -1) { + break; + } + } + + if (fingerprintSensorId == -1 || faceSensorId == -1) { + Log.e(TAG, "Missing fingerprint or face for dual-sensor config"); + mBiometricView = null; + mBackgroundView = null; + mBiometricScrollView = null; + return; + } + + FingerprintSensorPropertiesInternal fingerprintSensorProps = null; + for (FingerprintSensorPropertiesInternal prop : mFpProps) { + if (prop.sensorId == fingerprintSensorId) { + fingerprintSensorProps = prop; + break; + } + } + + if (fingerprintSensorProps != null && fingerprintSensorProps.isAnyUdfpsType()) { + final AuthBiometricFaceToUdfpsView faceToUdfpsView = + (AuthBiometricFaceToUdfpsView) factory.inflate( + R.layout.auth_biometric_face_to_udfps_view, null, false); + faceToUdfpsView.setFingerprintSensorProps(fingerprintSensorProps); + mBiometricView = faceToUdfpsView; + } else { + Log.e(TAG, "Fingerprint must be UDFPS for dual-sensor config"); + mBiometricView = null; + mBackgroundView = null; + mBiometricScrollView = null; + return; + } } else { - Log.e(TAG, "Unsupported sensor array, length: " + config.mSensorIds.length); + Log.e(TAG, "Unsupported sensor array, length: " + sensorCount); mBiometricView = null; mBackgroundView = null; mBiometricScrollView = null; @@ -442,14 +489,18 @@ public class AuthContainerView extends LinearLayout mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); break; + case Surface.ROTATION_90: mPanelController.setPosition(AuthPanelController.POSITION_RIGHT); setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT); break; + case Surface.ROTATION_270: mPanelController.setPosition(AuthPanelController.POSITION_LEFT); setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT); break; + + case Surface.ROTATION_180: default: Log.e(TAG, "Unsupported display rotation: " + displayRotation); mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java new file mode 100644 index 0000000000000..1ad2b9ca856cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java @@ -0,0 +1,330 @@ +/* + * 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.IdRes; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Insets; +import android.graphics.Rect; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.util.Log; +import android.view.Surface; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.R; + +/** + * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical + * under-display fingerprint sensor (UDFPS). + */ +public class UdfpsDialogMeasureAdapter { + private static final String TAG = "UdfpsDialogMeasurementAdapter"; + + @NonNull private final ViewGroup mView; + @NonNull private final FingerprintSensorPropertiesInternal mSensorProps; + + @Nullable private WindowManager mWindowManager; + + public UdfpsDialogMeasureAdapter( + @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) { + mView = view; + mSensorProps = sensorProps; + } + + @NonNull + FingerprintSensorPropertiesInternal getSensorProps() { + return mSensorProps; + } + + @NonNull + AuthDialog.LayoutParams onMeasureInternal( + int width, int height, @NonNull AuthDialog.LayoutParams layoutParams) { + + final int displayRotation = mView.getDisplay().getRotation(); + switch (displayRotation) { + case Surface.ROTATION_0: + return onMeasureInternalPortrait(width, height); + case Surface.ROTATION_90: + case Surface.ROTATION_270: + return onMeasureInternalLandscape(width, height); + default: + Log.e(TAG, "Unsupported display rotation: " + displayRotation); + return layoutParams; + } + } + + @NonNull + private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height) { + // Get the height of the everything below the icon. Currently, that's the indicator and + // button bar. + final int textIndicatorHeight = getViewHeightPx(R.id.indicator); + final int buttonBarHeight = getViewHeightPx(R.id.button_bar); + + // Figure out where the bottom of the sensor anim should be. + // Navbar + dialogMargin + buttonBar + textIndicator + spacerHeight = sensorDistFromBottom + final int dialogMargin = getDialogMarginPx(); + final int displayHeight = getWindowBounds().height(); + final Insets navbarInsets = getNavbarInsets(); + final int bottomSpacerHeight = calculateBottomSpacerHeightForPortrait( + mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight, + dialogMargin, navbarInsets.bottom); + + // Go through each of the children and do the custom measurement. + int totalHeight = 0; + final int numChildren = mView.getChildCount(); + final int sensorDiameter = mSensorProps.sensorRadius * 2; + for (int i = 0; i < numChildren; i++) { + final View child = mView.getChildAt(i); + if (child.getId() == R.id.biometric_icon_frame) { + final FrameLayout iconFrame = (FrameLayout) child; + final View icon = iconFrame.getChildAt(0); + + // Ensure that the icon is never larger than the sensor. + icon.measure( + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); + + // Create a frame that's exactly the height of the sensor circle. + iconFrame.measure( + MeasureSpec.makeMeasureSpec( + child.getLayoutParams().width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.space_above_icon) { + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + child.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.button_bar) { + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.space_below_icon) { + // Set the spacer height so the fingerprint icon is on the physical sensor area + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); + } else { + child.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + + if (child.getVisibility() != View.GONE) { + totalHeight += child.getMeasuredHeight(); + } + } + + return new AuthDialog.LayoutParams(width, totalHeight); + } + + @NonNull + private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height) { + // Find the spacer height needed to vertically align the icon with the sensor. + final int titleHeight = getViewHeightPx(R.id.title); + final int subtitleHeight = getViewHeightPx(R.id.subtitle); + final int descriptionHeight = getViewHeightPx(R.id.description); + final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon); + final int textIndicatorHeight = getViewHeightPx(R.id.indicator); + final int buttonBarHeight = getViewHeightPx(R.id.button_bar); + final Insets navbarInsets = getNavbarInsets(); + final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight, + subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight, + buttonBarHeight, navbarInsets.bottom); + + // Find the spacer width needed to horizontally align the icon with the sensor. + final int displayWidth = getWindowBounds().width(); + final int dialogMargin = getDialogMarginPx(); + final int horizontalInset = navbarInsets.left + navbarInsets.right; + final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape( + mSensorProps, displayWidth, dialogMargin, horizontalInset); + + final int sensorDiameter = mSensorProps.sensorRadius * 2; + final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth; + + int remeasuredHeight = 0; + final int numChildren = mView.getChildCount(); + for (int i = 0; i < numChildren; i++) { + final View child = mView.getChildAt(i); + if (child.getId() == R.id.biometric_icon_frame) { + final FrameLayout iconFrame = (FrameLayout) child; + final View icon = iconFrame.getChildAt(0); + + // Ensure that the icon is never larger than the sensor. + icon.measure( + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST)); + + // Create a frame that's exactly the height of the sensor circle. + iconFrame.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.space_above_icon || child.getId() == R.id.button_bar) { + // Adjust the width of the top spacer and button bar while preserving their heights. + child.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + child.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.space_below_icon) { + // Adjust the bottom spacer height to align the fingerprint icon with the sensor. + child.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY)); + } else { + // Use the remeasured width for all other child views. + child.measure( + MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST)); + } + + if (child.getVisibility() != View.GONE) { + remeasuredHeight += child.getMeasuredHeight(); + } + } + + return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight); + } + + private int getViewHeightPx(@IdRes int viewId) { + final View view = mView.findViewById(viewId); + return view != null ? view.getMeasuredHeight() : 0; + } + + private int getDialogMarginPx() { + return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding); + } + + @NonNull + private Insets getNavbarInsets() { + final WindowManager windowManager = getWindowManager(); + return windowManager != null && windowManager.getCurrentWindowMetrics() != null + ? windowManager.getCurrentWindowMetrics().getWindowInsets() + .getInsets(WindowInsets.Type.navigationBars()) + : Insets.NONE; + } + + @NonNull + private Rect getWindowBounds() { + final WindowManager windowManager = getWindowManager(); + return windowManager != null && windowManager.getCurrentWindowMetrics() != null + ? windowManager.getCurrentWindowMetrics().getBounds() + : new Rect(); + } + + @Nullable + private WindowManager getWindowManager() { + if (mWindowManager == null) { + mWindowManager = mView.getContext().getSystemService(WindowManager.class); + } + return mWindowManager; + } + + /** + * For devices in portrait orientation where the sensor is too high up, calculates the amount of + * padding necessary to center the biometric icon within the sensor's physical location. + */ + @VisibleForTesting + static int calculateBottomSpacerHeightForPortrait( + @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx, + int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx, + int navbarBottomInsetPx) { + + final int sensorDistanceFromBottom = displayHeightPx + - sensorProperties.sensorLocationY + - sensorProperties.sensorRadius; + + final int spacerHeight = sensorDistanceFromBottom + - textIndicatorHeightPx + - buttonBarHeightPx + - dialogMarginPx + - navbarBottomInsetPx; + + Log.d(TAG, "Display height: " + displayHeightPx + + ", Distance from bottom: " + sensorDistanceFromBottom + + ", Bottom margin: " + dialogMarginPx + + ", Navbar bottom inset: " + navbarBottomInsetPx + + ", Bottom spacer height (portrait): " + spacerHeight); + + return spacerHeight; + } + + /** + * For devices in landscape orientation where the sensor is too high up, calculates the amount + * of padding necessary to center the biometric icon within the sensor's physical location. + */ + @VisibleForTesting + static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx, + int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx, + int buttonBarHeightPx, int navbarBottomInsetPx) { + + final int dialogHeightAboveIcon = titleHeightPx + + subtitleHeightPx + + descriptionHeightPx + + topSpacerHeightPx; + + final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx; + + final int bottomSpacerHeight = dialogHeightAboveIcon + - dialogHeightBelowIcon + - navbarBottomInsetPx; + + Log.d(TAG, "Title height: " + titleHeightPx + + ", Subtitle height: " + subtitleHeightPx + + ", Description height: " + descriptionHeightPx + + ", Top spacer height: " + topSpacerHeightPx + + ", Text indicator height: " + textIndicatorHeightPx + + ", Button bar height: " + buttonBarHeightPx + + ", Navbar bottom inset: " + navbarBottomInsetPx + + ", Bottom spacer height (landscape): " + bottomSpacerHeight); + + return bottomSpacerHeight; + } + + /** + * For devices in landscape orientation where the sensor is too left/right, calculates the + * amount of padding necessary to center the biometric icon within the sensor's physical + * location. + */ + @VisibleForTesting + static int calculateHorizontalSpacerWidthForLandscape( + @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx, + int dialogMarginPx, int navbarHorizontalInsetPx) { + + final int sensorDistanceFromEdge = displayWidthPx + - sensorProperties.sensorLocationY + - sensorProperties.sensorRadius; + + final int horizontalPadding = sensorDistanceFromEdge + - dialogMarginPx + - navbarHorizontalInsetPx; + + Log.d(TAG, "Display width: " + displayWidthPx + + ", Distance from edge: " + sensorDistanceFromEdge + + ", Dialog margin: " + dialogMarginPx + + ", Navbar horizontal inset: " + navbarHorizontalInsetPx + + ", Horizontal spacer width (landscape): " + horizontalPadding); + + return horizontalPadding; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java index 1565dee932cdc..36a031a6ae355 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthBiometricViewTest.java @@ -26,11 +26,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.content.Context; -import android.hardware.biometrics.ComponentInfoInternal; import android.hardware.biometrics.PromptInfo; -import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; @@ -50,9 +46,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; -import java.util.List; - @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest @@ -334,86 +327,6 @@ public class AuthBiometricViewTest extends SysuiTestCase { verify(mCallback).onAction(AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL); } - @Test - public void testUdfpsBottomSpacerHeightForPortrait() { - final int displayHeightPx = 3000; - final int navbarHeightPx = 10; - final int dialogBottomMarginPx = 20; - final int buttonBarHeightPx = 100; - final int textIndicatorHeightPx = 200; - - final int sensorLocationX = 540; - final int sensorLocationY = 1600; - final int sensorRadius = 100; - - final List componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* resetLockoutRequiresHardwareAuthToken */, sensorLocationX, sensorLocationY, - sensorRadius); - - assertEquals(970, - AuthBiometricUdfpsView.calculateBottomSpacerHeightForPortrait( - props, displayHeightPx, textIndicatorHeightPx, buttonBarHeightPx, - dialogBottomMarginPx, navbarHeightPx - )); - } - - @Test - public void testUdfpsBottomSpacerHeightForLandscape() { - final int titleHeightPx = 320; - final int subtitleHeightPx = 240; - final int descriptionHeightPx = 200; - final int topSpacerHeightPx = 550; - final int textIndicatorHeightPx = 190; - final int buttonBarHeightPx = 160; - final int navbarBottomInsetPx = 75; - - assertEquals(885, - AuthBiometricUdfpsView.calculateBottomSpacerHeightForLandscape( - titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx, - textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx)); - } - - @Test - public void testUdfpsHorizontalSpacerWidthForLandscape() { - final int displayWidthPx = 3000; - final int dialogMarginPx = 20; - final int navbarHorizontalInsetPx = 75; - - final int sensorLocationX = 540; - final int sensorLocationY = 1600; - final int sensorRadius = 100; - - final List componentInfo = new ArrayList<>(); - componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, - "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, - "00000001" /* serialNumber */, "" /* softwareVersion */)); - componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, - "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, - "vendor/version/revision" /* softwareVersion */)); - - final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, - componentInfo, - FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, - true /* resetLockoutRequiresHardwareAuthToken */, sensorLocationX, sensorLocationY, - sensorRadius); - - assertEquals(1205, - AuthBiometricUdfpsView.calculateHorizontalSpacerWidthForLandscape( - props, displayWidthPx, dialogMarginPx, navbarHorizontalInsetPx)); - } - private PromptInfo buildPromptInfo(boolean allowDeviceCredential) { PromptInfo promptInfo = new PromptInfo(); promptInfo.setTitle("Title"); diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java new file mode 100644 index 0000000000000..ee13d23662851 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java @@ -0,0 +1,119 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import android.hardware.biometrics.ComponentInfoInternal; +import android.hardware.biometrics.SensorProperties; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class UdfpsDialogMeasureAdapterTest extends SysuiTestCase { + @Test + public void testUdfpsBottomSpacerHeightForPortrait() { + final int displayHeightPx = 3000; + final int navbarHeightPx = 10; + final int dialogBottomMarginPx = 20; + final int buttonBarHeightPx = 100; + final int textIndicatorHeightPx = 200; + + final int sensorLocationX = 540; + final int sensorLocationY = 1600; + final int sensorRadius = 100; + + final List componentInfo = new ArrayList<>(); + componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */)); + + final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( + 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, + componentInfo, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + true /* resetLockoutRequiresHardwareAuthToken */, sensorLocationX, sensorLocationY, + sensorRadius); + + assertEquals(970, + UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForPortrait( + props, displayHeightPx, textIndicatorHeightPx, buttonBarHeightPx, + dialogBottomMarginPx, navbarHeightPx + )); + } + + @Test + public void testUdfpsBottomSpacerHeightForLandscape() { + final int titleHeightPx = 320; + final int subtitleHeightPx = 240; + final int descriptionHeightPx = 200; + final int topSpacerHeightPx = 550; + final int textIndicatorHeightPx = 190; + final int buttonBarHeightPx = 160; + final int navbarBottomInsetPx = 75; + + assertEquals(885, + UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape( + titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx, + textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx)); + } + + @Test + public void testUdfpsHorizontalSpacerWidthForLandscape() { + final int displayWidthPx = 3000; + final int dialogMarginPx = 20; + final int navbarHorizontalInsetPx = 75; + + final int sensorLocationX = 540; + final int sensorLocationY = 1600; + final int sensorRadius = 100; + + final List componentInfo = new ArrayList<>(); + componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */, + "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */, + "00000001" /* serialNumber */, "" /* softwareVersion */)); + componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */, + "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */, + "vendor/version/revision" /* softwareVersion */)); + + final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal( + 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */, + componentInfo, + FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, + true /* resetLockoutRequiresHardwareAuthToken */, sensorLocationX, sensorLocationY, + sensorRadius); + + assertEquals(1205, + UdfpsDialogMeasureAdapter.calculateHorizontalSpacerWidthForLandscape( + props, displayWidthPx, dialogMarginPx, navbarHorizontalInsetPx)); + } +}