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:
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user