Refactor FingerprintEnrollFindSensor

Refactor FingerprintEnrollFindSensor to 3 pages for different sensor
types, and apply MVVM for them.

Bug: 259664912
Bug: 260957195
Bug: 260957816
Test: atest FingerprintRepositoryTest FingerprintEnrollmentActivityTest
      AutoCredentialViewModelTest FingerprintEnrollIntroViewModelTest

Change-Id: Iace790952567cac13e61e5175e90555d4da7dfe2
This commit is contained in:
Milton Wu
2022-11-18 13:33:02 +08:00
parent b8926bd868
commit 3be7385d90
24 changed files with 1706 additions and 180 deletions

View File

@@ -253,7 +253,11 @@ public class AutoCredentialViewModel extends AndroidViewModel {
if (isUnspecifiedPassword()) {
return CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
} else if (mCredentialModel.isValidGkPwHandle()) {
generateChallenge(mCredentialModel.getGkPwHandle());
final long gkPwHandle = mCredentialModel.getGkPwHandle();
mCredentialModel.clearGkPwHandle();
// GkPwHandle is got through caller activity, we shall not revoke it after
// generateChallenge(). Let caller activity to make decision.
generateChallenge(gkPwHandle, false /* revokeGkPwHandle */);
mIsGeneratingChallengeDuringCheckingCredential = true;
return CREDENTIAL_IS_GENERATING_CHALLENGE;
} else {
@@ -261,7 +265,7 @@ public class AutoCredentialViewModel extends AndroidViewModel {
}
}
private void generateChallenge(long gkPwHandle) {
private void generateChallenge(long gkPwHandle, boolean revokeGkPwHandle) {
mChallengeGenerator.setCallback((sensorId, userId, challenge) -> {
try {
final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId);
@@ -274,11 +278,13 @@ public class AutoCredentialViewModel extends AndroidViewModel {
return;
}
mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle);
mCredentialModel.clearGkPwHandle();
if (revokeGkPwHandle) {
mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle);
}
if (DEBUG) {
Log.d(TAG, "generateChallenge " + mCredentialModel);
Log.d(TAG, "generateChallenge(), model:" + mCredentialModel
+ ", revokeGkPwHandle:" + revokeGkPwHandle);
}
// Check credential again
@@ -314,7 +320,9 @@ public class AutoCredentialViewModel extends AndroidViewModel {
if (data != null) {
final long gkPwHandle = result.getData().getLongExtra(
EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE);
generateChallenge(gkPwHandle);
// Revoke self requested GkPwHandle because it shall only used once inside this
// activity lifecycle.
generateChallenge(gkPwHandle, true /* revokeGkPwHandle */);
return true;
}
}
@@ -328,6 +336,14 @@ public class AutoCredentialViewModel extends AndroidViewModel {
return mCredentialModel.getUserId();
}
/**
* Get userId for this credential
*/
@Nullable
public byte[] getToken() {
return mCredentialModel.getToken();
}
@Nullable
private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId)
throws IllegalStateException {

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2023 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.settings.biometrics2.ui.viewmodel;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
import com.android.systemui.unfold.updates.FoldProvider;
import java.util.concurrent.Executor;
/**
* ViewModel explaining the fingerprint sensor location for fingerprint enrollment.
*/
public class DeviceFoldedViewModel extends ViewModel {
private static final String TAG = "DeviceFoldedViewModel";
@NonNull private final MutableLiveData<Boolean> mLiveData =
new MutableLiveData<>(null);
private final ScreenSizeFoldProvider mScreenSizeFoldProvider;
private final FoldProvider.FoldCallback mIsFoldedCallback = isFolded -> {
Log.d(TAG, "onFoldUpdated= " + isFolded);
mLiveData.postValue(isFolded);
};
public DeviceFoldedViewModel(@NonNull ScreenSizeFoldProvider screenSizeFoldProvider,
@NonNull Executor executor) {
super();
mScreenSizeFoldProvider = screenSizeFoldProvider;
mScreenSizeFoldProvider.registerCallback(mIsFoldedCallback, executor);
}
/**
* Returns FoldedLiveData
*/
public LiveData<Boolean> getLiveData() {
return mLiveData;
}
@Override
protected void onCleared() {
mScreenSizeFoldProvider.unregisterCallback(mIsFoldedCallback);
super.onCleared();
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright (C) 2022 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.settings.biometrics2.ui.viewmodel;
import static android.hardware.display.DisplayManager.DisplayListener;
import android.app.Application;
import android.hardware.display.DisplayManager;
import android.util.Log;
import android.view.DisplayInfo;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
/**
* ViewModel explaining the fingerprint sensor location for fingerprint enrollment.
*/
public class DeviceRotationViewModel extends AndroidViewModel {
private static final boolean DEBUG = false;
private static final String TAG = "RotationViewModel";
private final DisplayManager mDisplayManager;
@NonNull private final DisplayInfo mDisplayInfo = new DisplayInfo();
private final DisplayListener mDisplayListener = new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
final int rotation = getRotation();
if (DEBUG) {
Log.d(TAG, "onDisplayChanged(" + displayId + "), rotation:" + rotation);
}
mLiveData.postValue(rotation);
}
};
@NonNull private final MutableLiveData<Integer> mLiveData =
new MutableLiveData<>(getRotation());
public DeviceRotationViewModel(@NonNull Application application) {
super(application);
mDisplayManager = application.getSystemService(DisplayManager.class);
mDisplayManager.registerDisplayListener(mDisplayListener,
application.getMainThreadHandler());
}
/**
* Returns current rotation
*/
@Surface.Rotation
private int getRotation() {
getApplication().getDisplay().getDisplayInfo(mDisplayInfo);
return mDisplayInfo.rotation;
}
/**
* Returns RotationLiveData
*/
public LiveData<Integer> getLiveData() {
return mLiveData;
}
@Override
protected void onCleared() {
mDisplayManager.unregisterDisplayListener(mDisplayListener);
super.onCleared();
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2022 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.settings.biometrics2.ui.viewmodel;
import android.annotation.IntDef;
import android.app.Application;
import android.util.Log;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* ViewModel explaining the fingerprint sensor location for fingerprint enrollment.
*/
public class FingerprintEnrollFindSensorViewModel extends AndroidViewModel {
private static final boolean DEBUG = false;
private static final String TAG = "FingerprintEnrollFindSensorViewModel";
/**
* User clicks 'Skip' button on this page in Settings
*/
public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP = 0;
/**
* User clicks 'Skip' button on this page in SetupWizard flow
*/
public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG = 1;
/**
* User clicks 'Start' button on this page
*/
public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START = 2;
@IntDef(prefix = { "FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_" }, value = {
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP,
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG,
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START
})
@Retention(RetentionPolicy.SOURCE)
public @interface FingerprintEnrollFindSensorAction {}
private final AccessibilityManager mAccessibilityManager;
private boolean mIsSuw = false;
@NonNull private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
public FingerprintEnrollFindSensorViewModel(@NonNull Application application) {
super(application);
mAccessibilityManager = application.getSystemService(AccessibilityManager.class);
}
/**
* Sets isSetupWizard or not
*/
public void setIsSuw(boolean isSuw) {
mIsSuw = isSuw;
}
/**
* Returns action live data that user chooses
*/
public LiveData<Integer> getActionLiveData() {
return mActionLiveData;
}
/**
* Clear ActionLiveData to prevent get obsolete data
*/
public void clearActionLiveData() {
mActionLiveData.setValue(null);
}
/**
* User clicks skip button on dialog
*/
public void onSkipDialogButtonClick() {
final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP;
if (DEBUG) {
Log.d(TAG, "onSkipDialogButtonClick, post " + action);
}
mActionLiveData.postValue(action);
}
/**
* User clicks skip button
*/
public void onSkipButtonClick() {
final int action = mIsSuw
? FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG
: FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP;
if (DEBUG) {
Log.d(TAG, "onSkipButtonClick, post action " + action);
}
mActionLiveData.postValue(action);
}
/**
* User clicks start button
*/
public void onStartButtonClick() {
final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START;
if (DEBUG) {
Log.d(TAG, "onStartButtonClick, post action " + action);
}
mActionLiveData.postValue(action);
}
/**
* Returns the info about accessibility is enabled or not
*/
public boolean isAccessibilityEnabled() {
return mAccessibilityManager.isEnabled();
}
}

View File

@@ -24,7 +24,6 @@ import android.annotation.IntDef;
import android.app.Application;
import android.os.UserHandle;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
@@ -189,7 +188,7 @@ public class FingerprintEnrollIntroViewModel extends AndroidViewModel
/**
* User clicks next button
*/
public void onNextButtonClick(View ignoredView) {
public void onNextButtonClick() {
final Integer status = mEnrollableStatusLiveData.getValue();
switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) {
case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
@@ -206,7 +205,7 @@ public class FingerprintEnrollIntroViewModel extends AndroidViewModel
/**
* User clicks skip/cancel button
*/
public void onSkipOrCancelButtonClick(View ignoredView) {
public void onSkipOrCancelButtonClick() {
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (C) 2022 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.settings.biometrics2.ui.viewmodel;
import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_REMAINING;
import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_STEPS;
import android.app.Application;
import android.content.res.Resources;
import android.hardware.fingerprint.FingerprintManager.EnrollReason;
import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback;
import android.os.CancellationSignal;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.android.settings.R;
import com.android.settings.biometrics.fingerprint.FingerprintUpdater;
import com.android.settings.biometrics.fingerprint.MessageDisplayController;
import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
/**
* Progress ViewModel handles the state around biometric enrollment. It manages the state of
* enrollment throughout the activity lifecycle so the app can continue after an event like
* rotation.
*/
public class FingerprintEnrollProgressViewModel extends AndroidViewModel {
private static final boolean DEBUG = false;
private static final String TAG = "FingerprintEnrollProgressViewModel";
private final MutableLiveData<EnrollmentProgress> mProgressLiveData = new MutableLiveData<>(
new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING));
private byte[] mToken = null;
private int mUserId = UserHandle.myUserId();
private final FingerprintUpdater mFingerprintUpdater;
private final MessageDisplayController mMessageDisplayController;
private EnrollmentHelper mEnrollmentHelper;
private final EnrollmentCallback mEnrollmentCallback = new EnrollmentCallback() {
@Override
public void onEnrollmentProgress(int remaining) {
final int currentSteps = getSteps();
final EnrollmentProgress progress = new EnrollmentProgress(
currentSteps == INITIAL_STEPS ? remaining : getSteps(), remaining);
if (DEBUG) {
Log.d(TAG, "onEnrollmentProgress(" + remaining + "), steps: " + currentSteps
+ ", post progress as " + progress);
}
mProgressLiveData.postValue(progress);
// TODO set enrolling to false when remaining is 0 during implementing b/260957933
}
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
// TODO add LiveData for help message during implementing b/260957933
}
@Override
public void onEnrollmentError(int errMsgId, CharSequence errString) {
// TODO add LiveData for error message during implementing b/260957933
}
};
public FingerprintEnrollProgressViewModel(@NonNull Application application,
@NonNull FingerprintUpdater fingerprintUpdater) {
super(application);
mFingerprintUpdater = fingerprintUpdater;
final Resources res = application.getResources();
mMessageDisplayController =
res.getBoolean(R.bool.enrollment_message_display_controller_flag)
? new MessageDisplayController(
application.getMainThreadHandler(),
mEnrollmentCallback,
SystemClock.elapsedRealtimeClock(),
res.getInteger(R.integer.enrollment_help_minimum_time_display),
res.getInteger(R.integer.enrollment_progress_minimum_time_display),
res.getBoolean(R.bool.enrollment_progress_priority_over_help),
res.getBoolean(R.bool.enrollment_prioritize_acquire_messages),
res.getInteger(R.integer.enrollment_collect_time)) : null;
}
public void setToken(byte[] token) {
mToken = token;
}
public void setUserId(int userId) {
mUserId = userId;
}
/**
* clear progress
*/
public void clearProgressLiveData() {
mProgressLiveData.setValue(new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING));
}
public LiveData<EnrollmentProgress> getProgressLiveData() {
return mProgressLiveData;
}
/**
* Starts enrollment and return latest isEnrolling() result
*/
public boolean startEnrollment(@EnrollReason int reason) {
if (mToken == null) {
Log.e(TAG, "Null hardware auth token for enroll");
return false;
}
if (isEnrolling()) {
Log.w(TAG, "Enrolling has started, shall not start again");
return true;
}
mEnrollmentHelper = new EnrollmentHelper(
mMessageDisplayController != null
? mMessageDisplayController
: mEnrollmentCallback);
mEnrollmentHelper.startEnrollment(mFingerprintUpdater, mToken, mUserId, reason);
return true;
}
/**
* Cancels enrollment and return latest isEnrolling result
*/
public boolean cancelEnrollment() {
if (!isEnrolling() || mEnrollmentHelper == null) {
Log.e(TAG, "Fail to cancel enrollment, enrollmentController exist:"
+ (mEnrollmentHelper != null));
return false;
}
mEnrollmentHelper.cancelEnrollment();
mEnrollmentHelper = null;
return true;
}
public boolean isEnrolling() {
return (mEnrollmentHelper != null);
}
private int getSteps() {
return mProgressLiveData.getValue().getSteps();
}
/**
* This class is used to stop latest message from onEnrollmentError() after user cancelled
* enrollment. This class will not forward message anymore after mCancellationSignal is sent.
*/
private static class EnrollmentHelper extends EnrollmentCallback {
@NonNull private final EnrollmentCallback mEnrollmentCallback;
@Nullable private CancellationSignal mCancellationSignal = new CancellationSignal();
EnrollmentHelper(@NonNull EnrollmentCallback enrollmentCallback) {
mEnrollmentCallback = enrollmentCallback;
}
@Override
public void onEnrollmentError(int errMsgId, CharSequence errString) {
if (mCancellationSignal == null) {
return;
}
mEnrollmentCallback.onEnrollmentError(errMsgId, errString);
}
@Override
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
if (mCancellationSignal == null) {
return;
}
mEnrollmentCallback.onEnrollmentHelp(helpMsgId, helpString);
}
@Override
public void onEnrollmentProgress(int remaining) {
if (mCancellationSignal == null) {
return;
}
mEnrollmentCallback.onEnrollmentProgress(remaining);
}
/**
* Starts enrollment
*/
public boolean startEnrollment(@NonNull FingerprintUpdater fingerprintUpdater,
@NonNull byte[] token, int userId, @EnrollReason int reason) {
if (mCancellationSignal == null) {
// Not allow enrolling twice as same instance. Allocate a new instance for second
// enrollment.
return false;
}
fingerprintUpdater.enroll(token, mCancellationSignal, userId, this, reason);
return true;
}
/**
* Cancels current enrollment
*/
public void cancelEnrollment() {
final CancellationSignal cancellationSignal = mCancellationSignal;
mCancellationSignal = null;
if (cancellationSignal != null) {
cancellationSignal.cancel();
}
}
}
}

View File

@@ -124,8 +124,6 @@ public class FingerprintEnrollmentViewModel extends AndroidViewModel implements
}
}
private boolean isKeyguardSecure() {
return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure();
}
@@ -182,4 +180,18 @@ public class FingerprintEnrollmentViewModel extends AndroidViewModel implements
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get());
}
/**
* The first sensor type is UDFPS sensor or not
*/
public boolean canAssumeUdfps() {
return mFingerprintRepository.canAssumeUdfps();
}
/**
* The first sensor type is side fps sensor or not
*/
public boolean canAssumeSfps() {
return mFingerprintRepository.canAssumeSfps();
}
}