Fix BiometricPrompt layout for UDFPS in landscape

Makes some fixes to the logic for BiometricPrompt so that it better
supports UDFPS in landscape:
1. Fix measurement used for UDFPS icon placement in landscape
2. Move prompt to correct position when falling back to UDFPS
3. Properly save/restore state for the face -> fingerprint prompt

Test: atest com.android.systemui.biometrics
Test: Manually tested face and/or fingerprint auth w/ device rotation

Fixes: 190832486
Change-Id: Iac32e97f870998d9815817c348e23cfd9d020215
This commit is contained in:
Curtis Belmonte
2021-06-30 14:16:31 -07:00
parent 8fd5255fc4
commit 6b5600cc13
7 changed files with 194 additions and 48 deletions

View File

@@ -19,17 +19,19 @@ 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.Modality;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
@@ -87,11 +89,9 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
}
}
@Modality
private int mActiveSensorType = TYPE_FACE;
@Nullable
private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter;
@Modality private int mActiveSensorType = TYPE_FACE;
@Nullable private FingerprintSensorPropertiesInternal mFingerprintSensorProps;
@Nullable private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter;
public AuthBiometricFaceToFingerprintView(Context context) {
super(context);
@@ -106,14 +106,17 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
super(context, attrs, injector);
}
void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) {
if (!sensorProps.isAnyUdfpsType()) {
return;
}
@Modality
int getActiveSensorType() {
return mActiveSensorType;
}
if (mUdfpsMeasureAdapter == null || mUdfpsMeasureAdapter.getSensorProps() != sensorProps) {
mUdfpsMeasureAdapter = new UdfpsDialogMeasureAdapter(this, sensorProps);
}
boolean isFingerprintUdfps() {
return mFingerprintSensorProps.isAnyUdfpsType();
}
void setFingerprintSensorProps(@NonNull FingerprintSensorPropertiesInternal sensorProps) {
mFingerprintSensorProps = sensorProps;
}
@Override
@@ -193,8 +196,34 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
@NonNull
AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
final AuthDialog.LayoutParams layoutParams = super.onMeasureInternal(width, height);
return mUdfpsMeasureAdapter != null
? mUdfpsMeasureAdapter.onMeasureInternal(width, height, layoutParams)
return isFingerprintUdfps()
? getUdfpsMeasureAdapter().onMeasureInternal(width, height, layoutParams)
: layoutParams;
}
@NonNull
private UdfpsDialogMeasureAdapter getUdfpsMeasureAdapter() {
if (mUdfpsMeasureAdapter == null
|| mUdfpsMeasureAdapter.getSensorProps() != mFingerprintSensorProps) {
mUdfpsMeasureAdapter = new UdfpsDialogMeasureAdapter(this, mFingerprintSensorProps);
}
return mUdfpsMeasureAdapter;
}
@Override
public void onSaveState(@NonNull Bundle outState) {
super.onSaveState(outState);
outState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, mActiveSensorType);
outState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS, mFingerprintSensorProps);
}
@Override
public void restoreState(@Nullable Bundle savedState) {
super.restoreState(savedState);
if (savedState != null) {
mActiveSensorType = savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FACE);
mFingerprintSensorProps =
savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS);
}
}
}

View File

@@ -593,11 +593,13 @@ public abstract class AuthBiometricView extends LinearLayout {
}
public void onSaveState(@NonNull Bundle outState) {
outState.putInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY,
mConfirmButton.getVisibility());
outState.putInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY,
mTryAgainButton.getVisibility());
outState.putInt(AuthDialog.KEY_BIOMETRIC_STATE, mState);
outState.putString(AuthDialog.KEY_BIOMETRIC_INDICATOR_STRING,
mIndicatorView.getText().toString());
mIndicatorView.getText() != null ? mIndicatorView.getText().toString() : "");
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_ERROR_SHOWING,
mHandler.hasCallbacks(mResetErrorRunnable));
outState.putBoolean(AuthDialog.KEY_BIOMETRIC_INDICATOR_HELP_SHOWING,
@@ -754,9 +756,12 @@ public abstract class AuthBiometricView extends LinearLayout {
// Restore as much state as possible first
updateState(mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_STATE));
// Restore positive button state
// Restore positive button(s) state
mConfirmButton.setVisibility(
mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_CONFIRM_VISIBILITY));
mTryAgainButton.setVisibility(
mSavedState.getInt(AuthDialog.KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY));
}
}

View File

@@ -16,6 +16,7 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static android.hardware.biometrics.BiometricManager.BiometricMultiSensorMode;
import android.annotation.IntDef;
@@ -23,6 +24,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.graphics.PixelFormat;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.PromptInfo;
@@ -487,31 +489,8 @@ public class AuthContainerView extends LinearLayout
+ mConfig.mPromptInfo.getAuthenticators());
}
if (mBiometricView instanceof AuthBiometricUdfpsView) {
final int displayRotation = getDisplay().getRotation();
switch (displayRotation) {
case Surface.ROTATION_0:
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);
setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
break;
}
if (shouldUpdatePositionForUdfps()) {
updatePositionForUdfps();
}
if (mConfig.mSkipIntro) {
@@ -557,6 +536,48 @@ public class AuthContainerView extends LinearLayout
}
}
private boolean shouldUpdatePositionForUdfps() {
if (mBiometricView instanceof AuthBiometricUdfpsView) {
return true;
}
if (mBiometricView instanceof AuthBiometricFaceToFingerprintView) {
AuthBiometricFaceToFingerprintView faceToFingerprintView =
(AuthBiometricFaceToFingerprintView) mBiometricView;
return faceToFingerprintView.getActiveSensorType() == TYPE_FINGERPRINT
&& faceToFingerprintView.isFingerprintUdfps();
}
return false;
}
private void updatePositionForUdfps() {
final int displayRotation = getDisplay().getRotation();
switch (displayRotation) {
case Surface.ROTATION_0:
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);
setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
break;
}
}
private void setScrollViewGravity(int gravity) {
final FrameLayout.LayoutParams params =
(FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams();
@@ -605,6 +626,13 @@ public class AuthContainerView extends LinearLayout
@Override
public void onAuthenticationFailed(@Modality int modality, String failureReason) {
mBiometricView.onAuthenticationFailed(modality, failureReason);
if (mBiometricView instanceof AuthBiometricFaceToFingerprintView
&& ((AuthBiometricFaceToFingerprintView) mBiometricView).isFingerprintUdfps()
&& modality == BiometricAuthenticator.TYPE_FACE) {
updatePositionForUdfps();
mPanelView.invalidateOutline();
mBiometricView.requestLayout();
}
}
@Override

View File

@@ -35,6 +35,7 @@ public interface AuthDialog {
String KEY_BIOMETRIC_SHOWING = "biometric_showing";
String KEY_CREDENTIAL_SHOWING = "credential_showing";
String KEY_BIOMETRIC_CONFIRM_VISIBILITY = "confirm_visibility";
String KEY_BIOMETRIC_TRY_AGAIN_VISIBILITY = "try_agian_visibility";
String KEY_BIOMETRIC_STATE = "state";
String KEY_BIOMETRIC_INDICATOR_STRING = "indicator_string"; // error / help / hint
@@ -42,6 +43,9 @@ public interface AuthDialog {
String KEY_BIOMETRIC_INDICATOR_HELP_SHOWING = "hint_is_temporary";
String KEY_BIOMETRIC_DIALOG_SIZE = "size";
String KEY_BIOMETRIC_SENSOR_TYPE = "sensor_type";
String KEY_BIOMETRIC_SENSOR_PROPS = "sensor_props";
int SIZE_UNKNOWN = 0;
/**
* Minimal UI, showing only biometric icon.

View File

@@ -180,17 +180,25 @@ public class UdfpsDialogMeasureAdapter {
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.
} else if (child.getId() == R.id.space_above_icon) {
// Adjust the width and height of the top spacer if necessary.
final int newTopSpacerHeight = child.getLayoutParams().height
- Math.min(bottomSpacerHeight, 0);
child.measure(
MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY));
} else if (child.getId() == R.id.button_bar) {
// Adjust the width of the button bar while preserving its height.
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.
final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0);
child.measure(
MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(bottomSpacerHeight, MeasureSpec.EXACTLY));
MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY));
} else {
// Use the remeasured width for all other child views.
child.measure(
@@ -208,7 +216,7 @@ public class UdfpsDialogMeasureAdapter {
private int getViewHeightPx(@IdRes int viewId) {
final View view = mView.findViewById(viewId);
return view != null ? view.getMeasuredHeight() : 0;
return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0;
}
private int getDialogMarginPx() {

View File

@@ -17,8 +17,10 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
@@ -27,6 +29,11 @@ import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.hardware.biometrics.ComponentInfoInternal;
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;
import android.testing.TestableLooper;
@@ -35,6 +42,8 @@ import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
@@ -45,6 +54,9 @@ import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
import java.util.List;
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
@@ -170,6 +182,50 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
}
@Test
public void testOnSaveState() {
final FingerprintSensorPropertiesInternal sensorProps = createFingerprintSensorProps();
mFaceToFpView.setFingerprintSensorProps(sensorProps);
final Bundle savedState = new Bundle();
mFaceToFpView.onSaveState(savedState);
assertEquals(savedState.getInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE),
mFaceToFpView.getActiveSensorType());
assertEquals(savedState.getParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS), sensorProps);
}
@Test
public void testRestoreState() {
final Bundle savedState = new Bundle();
savedState.putInt(AuthDialog.KEY_BIOMETRIC_SENSOR_TYPE, TYPE_FINGERPRINT);
savedState.putParcelable(AuthDialog.KEY_BIOMETRIC_SENSOR_PROPS,
createFingerprintSensorProps());
mFaceToFpView.restoreState(savedState);
assertEquals(mFaceToFpView.getActiveSensorType(), TYPE_FINGERPRINT);
assertTrue(mFaceToFpView.isFingerprintUdfps());
}
@NonNull
private static FingerprintSensorPropertiesInternal createFingerprintSensorProps() {
final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
componentInfo.add(new ComponentInfoInternal("componentId", "hardwareVersion",
"firmwareVersion", "serialNumber", "softwareVersion"));
return new FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
componentInfo,
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
true /* resetLockoutRequiresHardwareAuthToken */,
540 /* sensorLocationX */,
1600 /* sensorLocationY */,
100 /* sensorRadius */);
}
public class TestableView extends AuthBiometricFaceToFingerprintView {
public TestableView(Context context) {
super(context, null, new MockInjector());

View File

@@ -72,7 +72,7 @@ public class UdfpsDialogMeasureAdapterTest extends SysuiTestCase {
}
@Test
public void testUdfpsBottomSpacerHeightForLandscape() {
public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceAboveIcon() {
final int titleHeightPx = 320;
final int subtitleHeightPx = 240;
final int descriptionHeightPx = 200;
@@ -87,6 +87,22 @@ public class UdfpsDialogMeasureAdapterTest extends SysuiTestCase {
textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx));
}
@Test
public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceBelowIcon() {
final int titleHeightPx = 315;
final int subtitleHeightPx = 160;
final int descriptionHeightPx = 75;
final int topSpacerHeightPx = 220;
final int textIndicatorHeightPx = 290;
final int buttonBarHeightPx = 360;
final int navbarBottomInsetPx = 205;
assertEquals(-85,
UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape(
titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx,
textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx));
}
@Test
public void testUdfpsHorizontalSpacerWidthForLandscape() {
final int displayWidthPx = 3000;