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
This commit is contained in:
Curtis Belmonte
2021-03-22 15:15:13 -07:00
parent 2ea75ab9ce
commit d1ae004bf0
10 changed files with 727 additions and 396 deletions

View File

@@ -44,7 +44,7 @@
<FrameLayout
android:id="@+id/biometric_icon_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
@@ -80,7 +80,7 @@
android:layout_height="88dp"
style="?android:attr/buttonBarStyle"
android:orientation="horizontal"
android:paddingTop="16dp">
android:paddingTop="24dp">
<Space android:id="@+id/leftSpacer"
android:layout_width="8dp"

View File

@@ -0,0 +1,25 @@
<!--
~ 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.
-->
<com.android.systemui.biometrics.AuthBiometricFaceToUdfpsView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/auth_biometric_contents"/>
</com.android.systemui.biometrics.AuthBiometricFaceToUdfpsView>

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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),

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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<ComponentInfoInternal> 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<ComponentInfoInternal> 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");

View File

@@ -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<ComponentInfoInternal> 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<ComponentInfoInternal> 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));
}
}