Fix condition where FP would require confirmation

Adds IconController#deactivate to allow clean transfer of view
ownership. Now we deactivate the face IconController once we know we
need to animate to the fingerprint state, and use the fingerprint
IconController to display subsequent animations.

This also fixes a hidden issue where the face IconController was
leaking (pulsing logic keeps running) even after BP is dismissed.

Bug: 194351208
Test: manual, for requireConfirmation=true and requireConfirmation=false
Test: atest com.android.systemui.biometrics

Change-Id: Iefd3df7a8706ce9213109862d509235a74aa8099
This commit is contained in:
Kevin Chyn
2021-07-28 17:30:10 -07:00
parent ab3f902ccf
commit 4815db0d69
4 changed files with 83 additions and 61 deletions

View File

@@ -93,6 +93,8 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
@Nullable private ModalityListener mModalityListener;
@Nullable private FingerprintSensorPropertiesInternal mFingerprintSensorProps;
@Nullable private UdfpsDialogMeasureAdapter mUdfpsMeasureAdapter;
@Nullable @VisibleForTesting UdfpsIconController mUdfpsIconController;
public AuthBiometricFaceToFingerprintView(Context context) {
super(context);
@@ -107,6 +109,12 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
super(context, attrs, injector);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mUdfpsIconController = new UdfpsIconController(mContext, mIconView, mIndicatorView);
}
@Modality
int getActiveSensorType() {
return mActiveSensorType;
@@ -167,36 +175,27 @@ public class AuthBiometricFaceToFingerprintView extends AuthBiometricFaceView {
return super.getStateForAfterError();
}
@Override
@NonNull
protected IconController getIconController() {
if (mActiveSensorType == TYPE_FINGERPRINT) {
if (!(mIconController instanceof UdfpsIconController)) {
mIconController = createUdfpsIconController();
}
return mIconController;
}
return super.getIconController();
}
@NonNull
protected IconController createUdfpsIconController() {
return new UdfpsIconController(getContext(), mIconView, mIndicatorView);
}
@Override
public void updateState(@BiometricState int newState) {
if (mState == STATE_HELP || mState == STATE_ERROR) {
@Modality final int currentType = mActiveSensorType;
mActiveSensorType = TYPE_FINGERPRINT;
if (mActiveSensorType == TYPE_FACE) {
if (newState == STATE_HELP || newState == STATE_ERROR) {
mActiveSensorType = TYPE_FINGERPRINT;
setRequireConfirmation(false);
mConfirmButton.setEnabled(false);
mConfirmButton.setVisibility(View.GONE);
setRequireConfirmation(false);
mConfirmButton.setEnabled(false);
mConfirmButton.setVisibility(View.GONE);
if (mModalityListener != null && currentType != mActiveSensorType) {
mModalityListener.onModalitySwitched(currentType, mActiveSensorType);
if (mModalityListener != null) {
mModalityListener.onModalitySwitched(TYPE_FACE, mActiveSensorType);
}
// Deactivate the face icon controller so it stops drawing to the view
mFaceIconController.deactivate();
// Then, activate this icon controller. We need to start in the "error" state
mUdfpsIconController.updateState(mState, newState);
}
} else { // Fingerprint
mUdfpsIconController.updateState(mState, newState);
}
super.updateState(newState);

View File

@@ -49,6 +49,7 @@ public class AuthBiometricFaceView extends AuthBiometricView {
protected Handler mHandler;
protected boolean mLastPulseLightToDark; // false = dark to light, true = light to dark
protected @BiometricState int mState;
protected boolean mDeactivated;
protected IconController(Context context, ImageView iconView, TextView textView) {
mContext = context;
@@ -67,6 +68,11 @@ public class AuthBiometricFaceView extends AuthBiometricView {
}
protected void animateIcon(int iconRes, boolean repeat) {
Log.d(TAG, "animateIcon, state: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
final AnimatedVectorDrawable icon =
(AnimatedVectorDrawable) mContext.getDrawable(iconRes);
mIconView.setImageDrawable(icon);
@@ -92,12 +98,26 @@ public class AuthBiometricFaceView extends AuthBiometricView {
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
Log.d(TAG, "onAnimationEnd, mState: " + mState + ", deactivated: " + mDeactivated);
if (mDeactivated) {
return;
}
if (mState == STATE_AUTHENTICATING || mState == STATE_HELP) {
pulseInNextDirection();
}
}
protected void deactivate() {
mDeactivated = true;
}
protected void updateState(int lastState, int newState) {
if (mDeactivated) {
Log.w(TAG, "Ignoring updateState when deactivated: " + newState);
return;
}
final boolean lastStateIsErrorIcon =
lastState == STATE_ERROR || lastState == STATE_HELP;
@@ -142,7 +162,7 @@ public class AuthBiometricFaceView extends AuthBiometricView {
}
}
protected IconController mIconController;
@Nullable @VisibleForTesting IconController mFaceIconController;
public AuthBiometricFaceView(Context context) {
this(context, null);
@@ -157,6 +177,12 @@ public class AuthBiometricFaceView extends AuthBiometricView {
super(context, attrs, injector);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mFaceIconController = new IconController(mContext, mIconView, mIndicatorView);
}
@Override
protected int getDelayAfterAuthenticatedDurationMs() {
return HIDE_DELAY_MS;
@@ -187,17 +213,9 @@ public class AuthBiometricFaceView extends AuthBiometricView {
return true;
}
@NonNull
protected IconController getIconController() {
if (mIconController == null) {
mIconController = new IconController(mContext, mIconView, mIndicatorView);
}
return mIconController;
}
@Override
public void updateState(@BiometricState int newState) {
getIconController().updateState(mState, newState);
mFaceIconController.updateState(mState, newState);
if (newState == STATE_AUTHENTICATING_ANIMATING_IN ||
(newState == STATE_AUTHENTICATING && getSize() == AuthDialog.SIZE_MEDIUM)) {

View File

@@ -23,7 +23,6 @@ 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;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -50,7 +49,6 @@ import com.android.systemui.SysuiTestCase;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -78,14 +76,16 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
@Mock private TextView mIndicatorView;
@Mock private ImageView mIconView;
@Mock private View mIconHolderView;
@Mock private AuthBiometricFaceView.IconController mIconController;
@Mock private AuthBiometricFaceView.IconController mFaceIconController;
@Mock private AuthBiometricFaceToFingerprintView.UdfpsIconController mUdfpsIconController;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceToFpView = new TestableView(mContext);
mFaceToFpView.mIconController = mIconController;
mFaceToFpView.mFaceIconController = mFaceIconController;
mFaceToFpView.mUdfpsIconController = mUdfpsIconController;
mFaceToFpView.setCallback(mCallback);
mFaceToFpView.mNegativeButton = mNegativeButton;
@@ -99,20 +99,23 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
@Test
public void testStateUpdated_whenDialogAnimatedIn() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mIconController)
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt(), anyInt());
}
@Test
public void testIconUpdatesState_whenDialogStateUpdated() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mIconController)
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt(), anyInt());
mFaceToFpView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED);
verify(mFaceToFpView.mIconController).updateState(
verify(mFaceToFpView.mFaceIconController).updateState(
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING),
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED));
verify(mFaceToFpView.mUdfpsIconController, never()).updateState(anyInt(), anyInt());
assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATED, mFaceToFpView.mState);
}
@@ -120,21 +123,22 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
@Test
public void testStateUpdated_whenSwitchToFingerprint() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mIconController)
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_ERROR);
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);
InOrder order = inOrder(mFaceToFpView.mIconController);
order.verify(mFaceToFpView.mIconController).updateState(
verify(mFaceToFpView.mFaceIconController).deactivate();
verify(mFaceToFpView.mUdfpsIconController).updateState(
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING),
eq(AuthBiometricFaceToFingerprintView.STATE_ERROR));
order.verify(mFaceToFpView.mIconController).updateState(
verify(mConfirmButton).setVisibility(eq(View.GONE));
mFaceToFpView.updateState(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING);
verify(mFaceToFpView.mUdfpsIconController).updateState(
eq(AuthBiometricFaceToFingerprintView.STATE_ERROR),
eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
verify(mConfirmButton).setVisibility(eq(View.GONE));
}
@Test
@@ -172,7 +176,10 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
verify(mCallback).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState);
// First we enter the error state, since we need to show the error animation/text. The
// error state is later cleared based on a timer, and we enter STATE_AUTHENTICATING.
assertEquals(AuthBiometricFaceToFingerprintView.STATE_ERROR, mFaceToFpView.mState);
}
@Test
@@ -185,13 +192,16 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
eq(mContext.getString(R.string.fingerprint_dialog_use_fingerprint_instead)));
verify(mCallback).onAction(
eq(AuthBiometricView.Callback.ACTION_START_DELAYED_FINGERPRINT_SENSOR));
assertEquals(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING, mFaceToFpView.mState);
// First we enter the error state, since we need to show the error animation/text. The
// error state is later cleared based on a timer, and we enter STATE_AUTHENTICATING.
assertEquals(AuthBiometricFaceToFingerprintView.STATE_ERROR, mFaceToFpView.mState);
}
@Test
public void testFingerprintOnlyStartsOnFirstError() {
mFaceToFpView.onDialogAnimatedIn();
verify(mFaceToFpView.mIconController)
verify(mFaceToFpView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceToFingerprintView.STATE_AUTHENTICATING));
mFaceToFpView.onDialogAnimatedIn();
@@ -260,11 +270,6 @@ public class AuthBiometricFaceToFingerprintViewTest extends SysuiTestCase {
protected int getDelayAfterAuthenticatedDurationMs() {
return 0;
}
@Override
protected IconController createUdfpsIconController() {
return AuthBiometricFaceToFingerprintViewTest.this.mIconController;
}
}
private class MockInjector extends AuthBiometricView.Injector {

View File

@@ -62,7 +62,7 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
public void setup() {
MockitoAnnotations.initMocks(this);
mFaceView = new TestableFaceView(mContext);
mFaceView.mIconController = mock(TestableFaceView.TestableIconController.class);
mFaceView.mFaceIconController = mock(TestableFaceView.TestableIconController.class);
mFaceView.setCallback(mCallback);
mFaceView.mNegativeButton = mNegativeButton;
@@ -78,18 +78,18 @@ public class AuthBiometricFaceViewTest extends SysuiTestCase {
@Test
public void testStateUpdated_whenDialogAnimatedIn() {
mFaceView.onDialogAnimatedIn();
verify(mFaceView.mIconController)
verify(mFaceView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING));
}
@Test
public void testIconUpdatesState_whenDialogStateUpdated() {
mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATING);
verify(mFaceView.mIconController)
verify(mFaceView.mFaceIconController)
.updateState(anyInt(), eq(AuthBiometricFaceView.STATE_AUTHENTICATING));
mFaceView.updateState(AuthBiometricFaceView.STATE_AUTHENTICATED);
verify(mFaceView.mIconController).updateState(
verify(mFaceView.mFaceIconController).updateState(
eq(AuthBiometricFaceView.STATE_AUTHENTICATING),
eq(AuthBiometricFaceView.STATE_AUTHENTICATED));
}