Fingerprint Introduction FragmentActivity

Introducing MVVM architecture & fragments to biometric settings.
Here, we modify the first page of FingerprintEnrollIntroduction to use
new MVVM with Fragment architecture.

And with this new architecture, unit test and screen order will be
easier to be written or changed.

Bug: 236072782
Test: atest FingerprintEnrollmentViewModelTest AutoCredentialViewModelTest
	    FingerprintEnrollIntroViewModelTest FingerprintRepositoryTest
Change-Id: Icf12c91625db86c2c99081a0108203e607e77f74
This commit is contained in:
Milton Wu
2022-09-14 14:26:05 +08:00
parent 45be6bae05
commit b9b8b8a512
30 changed files with 3319 additions and 1 deletions

View File

@@ -2384,6 +2384,11 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".biometrics2.ui.view.FingerprintEnrollmentActivity"
android:exported="true"
android:permission="android.permission.MANAGE_FINGERPRINT"
android:theme="@style/GlifTheme.Light"/>
<activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal" <activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal"
android:exported="false" android:exported="false"
android:theme="@style/GlifTheme.Light" android:theme="@style/GlifTheme.Light"

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -39,6 +39,7 @@ import com.android.settings.SubSettings;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling; import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction; import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal; import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal;
import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
import com.android.settings.core.FeatureFlags; import com.android.settings.core.FeatureFlags;
import com.android.settings.homepage.DeepLinkHomepageActivity; import com.android.settings.homepage.DeepLinkHomepageActivity;
import com.android.settings.homepage.DeepLinkHomepageActivityInternal; import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
@@ -225,6 +226,7 @@ public class ActivityEmbeddingRulesController {
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE); .buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
addActivityFilter(activityFilters, searchIntent); addActivityFilter(activityFilters, searchIntent);
} }
addActivityFilter(activityFilters, FingerprintEnrollmentActivity.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class); addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class); addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class); addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);

View File

@@ -25,6 +25,7 @@ import android.content.IntentSender;
import android.hardware.biometrics.SensorProperties; import android.hardware.biometrics.SensorProperties;
import android.hardware.face.FaceManager; import android.hardware.face.FaceManager;
import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.face.FaceSensorPropertiesInternal;
import android.os.Bundle;
import android.os.storage.StorageManager; import android.os.storage.StorageManager;
import android.util.Log; import android.util.Log;
import android.view.Surface; import android.view.Surface;
@@ -145,6 +146,31 @@ public class BiometricUtils {
} }
} }
/**
* @param context caller's context
* @param isSuw if it is running in setup wizard flows
* @param suwExtras setup wizard extras for new intent
* @return Intent for starting ChooseLock*
*/
public static Intent getChooseLockIntent(@NonNull Context context,
boolean isSuw, @NonNull Bundle suwExtras) {
if (isSuw) {
// Default to PIN lock in setup wizard
Intent intent = new Intent(context, SetupChooseLockGeneric.class);
if (StorageManager.isFileEncrypted()) {
intent.putExtra(
LockPatternUtils.PASSWORD_TYPE_KEY,
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment
.EXTRA_SHOW_OPTIONS_BUTTON, true);
}
intent.putExtras(suwExtras);
return intent;
} else {
return new Intent(context, ChooseLockGeneric.class);
}
}
/** /**
* @param context caller's context * @param context caller's context
* @param activityIntent The intent that started the caller's activity * @param activityIntent The intent that started the caller's activity

View File

@@ -32,7 +32,7 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu
/** /**
* Returns the number of fingerprint enrolled. * Returns the number of fingerprint enrolled.
*/ */
private static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count"; public static final String EXTRA_FINGERPRINT_ENROLLED_COUNT = "fingerprint_enrolled_count";
private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent"; private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent";

View File

@@ -0,0 +1,108 @@
/*
* 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.data.repository;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.R;
import com.android.settings.biometrics.ParentalControlsUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;
import java.util.List;
/**
* This repository is used to call all APIs in {@link FingerprintManager}
*/
public class FingerprintRepository {
@NonNull private final FingerprintManager mFingerprintManager;
public FingerprintRepository(@NonNull FingerprintManager fingerprintManager) {
mFingerprintManager = fingerprintManager;
}
/**
* The first sensor type is UDFPS sensor or not
*/
public boolean canAssumeUdfps() {
FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
return prop != null && prop.isAnyUdfpsType();
}
/**
* Get max possible number of fingerprints for a user
*/
public int getMaxFingerprints() {
FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
return prop != null ? prop.maxEnrollmentsPerUser : 0;
}
/**
* Get number of fingerprints that this user enrolled.
*/
public int getNumOfEnrolledFingerprintsSize(int userId) {
final List<Fingerprint> list = mFingerprintManager.getEnrolledFingerprints(userId);
return list != null ? list.size() : 0;
}
/**
* Get maximum possible fingerprints in setup wizard flow
*/
public int getMaxFingerprintsInSuw(@NonNull Resources resources) {
return resources.getInteger(R.integer.suw_max_fingerprints_enrollable);
}
@Nullable
private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
final List<FingerprintSensorPropertiesInternal> props =
mFingerprintManager.getSensorPropertiesInternal();
return props.size() > 0 ? props.get(0) : null;
}
/**
* Call FingerprintManager to generate challenge for first sensor
*/
public void generateChallenge(int userId,
@NonNull FingerprintManager.GenerateChallengeCallback callback) {
mFingerprintManager.generateChallenge(userId, callback);
}
/**
* Get parental consent required or not during enrollment process
*/
public boolean isParentalConsentRequired(@NonNull Context context) {
return ParentalControlsUtils.parentConsentRequired(context, TYPE_FINGERPRINT) != null;
}
/**
* Get fingerprint is disable by admin or not
*/
public boolean isDisabledByAdmin(@NonNull Context context, int userId) {
return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null;
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.factory;
import android.app.Application;
import android.app.admin.DevicePolicyManager;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentFactory;
import androidx.lifecycle.ViewModelProvider;
import com.android.settings.biometrics2.ui.view.FingerprintEnrollIntroFragment;
/**
* Fragment factory for biometrics
*/
public class BiometricsFragmentFactory extends FragmentFactory {
private final Application mApplication;
private final ViewModelProvider mViewModelProvider;
public BiometricsFragmentFactory(Application application,
ViewModelProvider viewModelProvider) {
mApplication = application;
mViewModelProvider = viewModelProvider;
}
@NonNull
@Override
public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
final Class<? extends Fragment> clazz = loadFragmentClass(classLoader, className);
if (FingerprintEnrollIntroFragment.class.equals(clazz)) {
final DevicePolicyManager devicePolicyManager =
mApplication.getSystemService(DevicePolicyManager.class);
if (devicePolicyManager != null) {
return new FingerprintEnrollIntroFragment(mViewModelProvider,
devicePolicyManager.getResources());
}
}
return super.instantiate(classLoader, className);
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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.factory;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
/**
* Interface for BiometricsRepositoryProvider
*/
public interface BiometricsRepositoryProvider {
/**
* Get FingerprintRepository
*/
@Nullable
FingerprintRepository getFingerprintRepository(@NonNull Application application);
}

View File

@@ -0,0 +1,46 @@
/*
* 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.factory;
import android.app.Application;
import android.hardware.fingerprint.FingerprintManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.Utils;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
/**
* Implementation for BiometricsRepositoryProvider
*/
public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider {
/**
* Get FingerprintRepository
*/
@Nullable
@Override
public FingerprintRepository getFingerprintRepository(@NonNull Application application) {
final FingerprintManager fingerprintManager =
Utils.getFingerprintManagerOrNull(application);
if (fingerprintManager == null) {
return null;
}
return new FingerprintRepository(fingerprintManager);
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.factory;
import android.app.Application;
import android.app.KeyguardManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory;
import androidx.lifecycle.viewmodel.CreationExtras;
import com.android.internal.widget.LockPatternUtils;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
import com.android.settings.overlay.FeatureFactory;
/**
* View model factory for biometric enrollment fragment
*/
public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
private static final String TAG = "BiometricsViewModelFact";
public static final CreationExtras.Key<ChallengeGenerator> CHALLENGE_GENERATOR =
new CreationExtras.Key<>() {};
@NonNull
@Override
@SuppressWarnings("unchecked")
public <T extends ViewModel> T create(@NonNull Class<T> modelClass,
@NonNull CreationExtras extras) {
final Application application = extras.get(AndroidViewModelFactory.APPLICATION_KEY);
if (application == null) {
Log.w(TAG, "create, null application");
return create(modelClass);
}
final FeatureFactory featureFactory = FeatureFactory.getFactory(application);
final BiometricsRepositoryProvider provider = FeatureFactory.getFactory(application)
.getBiometricsRepositoryProvider();
if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
final FingerprintRepository repository = provider.getFingerprintRepository(application);
if (repository != null) {
return (T) new FingerprintEnrollIntroViewModel(application, repository);
}
} else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
final FingerprintRepository repository = provider.getFingerprintRepository(application);
if (repository != null) {
return (T) new FingerprintEnrollmentViewModel(application, repository,
application.getSystemService(KeyguardManager.class));
}
} else if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) {
final LockPatternUtils lockPatternUtils =
featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application);
final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR);
if (challengeGenerator != null) {
return (T) new AutoCredentialViewModel(application, lockPatternUtils,
challengeGenerator);
}
}
return create(modelClass);
}
}

View File

@@ -0,0 +1,202 @@
/*
* 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.model;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
import android.content.Intent;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.time.Clock;
/**
* Secret credential data including
* 1. userId
* 2. sensorId
* 3. challenge
* 4. token
* 5. gkPwHandle
*/
public final class CredentialModel {
/**
* Default value for an invalid challenge
*/
@VisibleForTesting
public static final long INVALID_CHALLENGE = -1L;
/**
* Default value if GkPwHandle is invalid.
*/
public static final long INVALID_GK_PW_HANDLE = 0L;
/**
* Default value for a invalid sensor id
*/
@VisibleForTesting
public static final int INVALID_SENSOR_ID = -1;
private final Clock mClock;
private final long mInitMillis;
private final int mUserId;
private int mSensorId;
@Nullable
private Long mUpdateSensorIdMillis = null;
private long mChallenge;
@Nullable
private Long mUpdateChallengeMillis = null;
@Nullable
private byte[] mToken;
@Nullable
private Long mUpdateTokenMillis = null;
private long mGkPwHandle;
@Nullable
private Long mClearGkPwHandleMillis = null;
public CredentialModel(@NonNull Intent intent, @NonNull Clock clock) {
mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.myUserId());
mSensorId = intent.getIntExtra(EXTRA_KEY_SENSOR_ID, INVALID_SENSOR_ID);
mChallenge = intent.getLongExtra(EXTRA_KEY_CHALLENGE, INVALID_CHALLENGE);
mToken = intent.getByteArrayExtra(EXTRA_KEY_CHALLENGE_TOKEN);
mGkPwHandle = intent.getLongExtra(EXTRA_KEY_GK_PW_HANDLE,
INVALID_GK_PW_HANDLE);
mClock = clock;
mInitMillis = mClock.millis();
}
/**
* Get userId for this credential
*/
public int getUserId() {
return mUserId;
}
/**
* Check user id is valid or not
*/
public static boolean isValidUserId(int userId) {
return userId != UserHandle.USER_NULL;
}
/**
* Get challenge
*/
public long getChallenge() {
return mChallenge;
}
/**
* Set challenge
*/
public void setChallenge(long value) {
mUpdateChallengeMillis = mClock.millis();
mChallenge = value;
}
/**
* Get challenge token
*/
@Nullable
public byte[] getToken() {
return mToken;
}
/**
* Set challenge token
*/
public void setToken(@Nullable byte[] value) {
mUpdateTokenMillis = mClock.millis();
mToken = value;
}
/**
* Check challengeToken is valid or not
*/
public static boolean isValidToken(@Nullable byte[] token) {
return token != null;
}
/**
* Get gatekeeper password handle
*/
public long getGkPwHandle() {
return mGkPwHandle;
}
/**
* Clear gatekeeper password handle data
*/
public void clearGkPwHandle() {
mClearGkPwHandleMillis = mClock.millis();
mGkPwHandle = INVALID_GK_PW_HANDLE;
}
/**
* Check gkPwHandle is valid or not
*/
public static boolean isValidGkPwHandle(long gkPwHandle) {
return gkPwHandle != INVALID_GK_PW_HANDLE;
}
/**
* Get sensor id
*/
public int getSensorId() {
return mSensorId;
}
/**
* Set sensor id
*/
public void setSensorId(int value) {
mUpdateSensorIdMillis = mClock.millis();
mSensorId = value;
}
/**
* Returns a string representation of the object
*/
@Override
public String toString() {
final int gkPwHandleLen = ("" + mGkPwHandle).length();
final int tokenLen = mToken == null ? 0 : mToken.length;
final int challengeLen = ("" + mChallenge).length();
return getClass().getSimpleName() + ":{initMillis:" + mInitMillis
+ ", userId:" + mUserId
+ ", challenge:{len:" + challengeLen
+ ", updateMillis:" + mUpdateChallengeMillis + "}"
+ ", token:{len:" + tokenLen + ", isValid:" + isValidToken(mToken)
+ ", updateMillis:" + mUpdateTokenMillis + "}"
+ ", gkPwHandle:{len:" + gkPwHandleLen + ", isValid:"
+ isValidGkPwHandle(mGkPwHandle) + ", clearMillis:" + mClearGkPwHandleMillis + "}"
+ ", mSensorId:{id:" + mSensorId + ", updateMillis:" + mUpdateSensorIdMillis + "}"
+ " }";
}
}

View File

@@ -0,0 +1,99 @@
/*
* 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.model;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.android.settings.SetupWizardUtils;
import com.google.android.setupcompat.util.WizardManagerHelper;
/**
* Biometric enrollment generic intent data, which includes
* 1. isSuw
* 2. isAfterSuwOrSuwSuggestedAction
* 3. theme
* 4. isFromSettingsSummery
* 5. a helper method, getSetupWizardExtras
*/
public final class EnrollmentRequest {
private final boolean mIsSuw;
private final boolean mIsAfterSuwOrSuwSuggestedAction;
private final boolean mIsFromSettingsSummery;
private final int mTheme;
private final Bundle mSuwExtras;
public EnrollmentRequest(@NonNull Intent intent, @NonNull Context context) {
mIsSuw = WizardManagerHelper.isAnySetupWizard(intent);
mIsAfterSuwOrSuwSuggestedAction = WizardManagerHelper.isDeferredSetupWizard(intent)
|| WizardManagerHelper.isPortalSetupWizard(intent)
|| intent.getBooleanExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, false);
mSuwExtras = getSuwExtras(mIsSuw, intent);
mIsFromSettingsSummery = intent.getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false);
mTheme = SetupWizardUtils.getTheme(context, intent);
}
public boolean isSuw() {
return mIsSuw;
}
public boolean isAfterSuwOrSuwSuggestedAction() {
return mIsAfterSuwOrSuwSuggestedAction;
}
public boolean isFromSettingsSummery() {
return mIsFromSettingsSummery;
}
public int getTheme() {
return mTheme;
}
@NonNull
public Bundle getSuwExtras() {
return new Bundle(mSuwExtras);
}
/**
* Returns a string representation of the object
*/
@Override
public String toString() {
return getClass().getSimpleName() + ":{isSuw:" + mIsSuw
+ ", isAfterSuwOrSuwSuggestedAction:" + mIsAfterSuwOrSuwSuggestedAction
+ ", isFromSettingsSummery:" + mIsFromSettingsSummery
+ "}";
}
@NonNull
private static Bundle getSuwExtras(boolean isSuw, @NonNull Intent intent) {
final Intent toIntent = new Intent();
if (isSuw) {
SetupWizardUtils.copySetupExtras(intent, toIntent);
}
return toIntent.getExtras() != null ? toIntent.getExtras() : new Bundle();
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.model;
import android.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Fingerprint onboarding introduction page data, it contains following information which needs
* to be passed from view model to view.
* 1. mEnrollableStatus: User is allowed to enroll a new fingerprint or not.
* 2. mHasScrollToBottom: User has scrolled to the bottom of this page or not.
*/
public final class FingerprintEnrollIntroStatus {
/**
* Unconfirmed case, it means that this value is invalid, and view shall bypass this value.
*/
public static final int FINGERPRINT_ENROLLABLE_UNKNOWN = -1;
/**
* User is allowed to enrolled a new fingerprint.
*/
public static final int FINGERPRINT_ENROLLABLE_OK = 0;
/**
* User is not allowed to enrolled a new fingerprint because the number of enrolled fingerprint
* has reached maximum.
*/
public static final int FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX = 1;
@IntDef(prefix = {"FINGERPRINT_ENROLLABLE_"}, value = {
FINGERPRINT_ENROLLABLE_UNKNOWN,
FINGERPRINT_ENROLLABLE_OK,
FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
})
@Retention(RetentionPolicy.SOURCE)
public @interface FingerprintEnrollableStatus {
}
private final boolean mHasScrollToBottom;
@FingerprintEnrollableStatus
private final int mEnrollableStatus;
public FingerprintEnrollIntroStatus(boolean hasScrollToBottom, int enrollableStatus) {
mEnrollableStatus = enrollableStatus;
mHasScrollToBottom = hasScrollToBottom;
}
/**
* Get enrollable status. It means that user is allowed to enroll a new fingerprint or not.
*/
@FingerprintEnrollableStatus
public int getEnrollableStatus() {
return mEnrollableStatus;
}
/**
* Get info for this onboarding introduction page has scrolled to bottom or not
*/
public boolean hasScrollToBottom() {
return mHasScrollToBottom;
}
}

View File

@@ -0,0 +1,288 @@
/*
* 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.view;
import static android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
import static com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT;
import android.app.Activity;
import android.app.admin.DevicePolicyResourcesManager;
import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Bundle;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.android.settings.R;
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupdesign.GlifLayout;
import com.google.android.setupdesign.template.RequireScrollMixin;
import com.google.android.setupdesign.util.DynamicColorPalette;
/**
* Fingerprint intro onboarding page fragment implementation
*/
public class FingerprintEnrollIntroFragment extends Fragment {
private static final String TAG = "FingerprintEnrollIntroFragment";
@NonNull private final ViewModelProvider mViewModelProvider;
@Nullable private final DevicePolicyResourcesManager mDevicePolicyMgrRes;
private FingerprintEnrollIntroViewModel mViewModel = null;
private View mView = null;
private FooterButton mPrimaryFooterButton = null;
private FooterButton mSecondaryFooterButton = null;
private ImageView mIconShield = null;
private TextView mFooterMessage6 = null;
@Nullable private PorterDuffColorFilter mIconColorFilter;
public FingerprintEnrollIntroFragment(
@NonNull ViewModelProvider viewModelProvider,
@Nullable DevicePolicyResourcesManager devicePolicyMgrRes) {
super();
mViewModelProvider = viewModelProvider;
mDevicePolicyMgrRes = devicePolicyMgrRes;
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
final Context context = inflater.getContext();
mView = inflater.inflate(R.layout.fingerprint_enroll_introduction, container);
final ImageView iconFingerprint = mView.findViewById(R.id.icon_fingerprint);
final ImageView iconDeviceLocked = mView.findViewById(R.id.icon_device_locked);
final ImageView iconTrashCan = mView.findViewById(R.id.icon_trash_can);
final ImageView iconInfo = mView.findViewById(R.id.icon_info);
mIconShield = mView.findViewById(R.id.icon_shield);
final ImageView iconLink = mView.findViewById(R.id.icon_link);
iconFingerprint.getDrawable().setColorFilter(getIconColorFilter(context));
iconDeviceLocked.getDrawable().setColorFilter(getIconColorFilter(context));
iconTrashCan.getDrawable().setColorFilter(getIconColorFilter(context));
iconInfo.getDrawable().setColorFilter(getIconColorFilter(context));
mIconShield.getDrawable().setColorFilter(getIconColorFilter(context));
iconLink.getDrawable().setColorFilter(getIconColorFilter(context));
final TextView footerMessage2 = mView.findViewById(R.id.footer_message_2);
final TextView footerMessage3 = mView.findViewById(R.id.footer_message_3);
final TextView footerMessage4 = mView.findViewById(R.id.footer_message_4);
final TextView footerMessage5 = mView.findViewById(R.id.footer_message_5);
mFooterMessage6 = mView.findViewById(R.id.footer_message_6);
footerMessage2.setText(
R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_2);
footerMessage3.setText(
R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_3);
footerMessage4.setText(
R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_4);
footerMessage5.setText(
R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_5);
mFooterMessage6.setText(
R.string.security_settings_fingerprint_v2_enroll_introduction_footer_message_6);
final TextView footerTitle1 = mView.findViewById(R.id.footer_title_1);
final TextView footerTitle2 = mView.findViewById(R.id.footer_title_2);
footerTitle1.setText(
R.string.security_settings_fingerprint_enroll_introduction_footer_title_1);
footerTitle2.setText(
R.string.security_settings_fingerprint_enroll_introduction_footer_title_2);
return mView;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
final Context context = view.getContext();
final TextView footerLink = mView.findViewById(R.id.footer_learn_more);
footerLink.setMovementMethod(LinkMovementMethod.getInstance());
final String footerLinkStr = getContext().getString(
R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more,
Html.FROM_HTML_MODE_LEGACY);
footerLink.setText(Html.fromHtml(footerLinkStr));
// footer buttons
mPrimaryFooterButton = new FooterButton.Builder(context)
.setText(R.string.security_settings_fingerprint_enroll_introduction_agree)
.setListener(mViewModel::onNextButtonClick)
.setButtonType(FooterButton.ButtonType.OPT_IN)
.setTheme(R.style.SudGlifButton_Primary)
.build();
mSecondaryFooterButton = new FooterButton.Builder(context)
.setListener(mViewModel::onSkipOrCancelButtonClick)
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(R.style.SudGlifButton_Primary)
.build();
getFooterBarMixin().setPrimaryButton(mPrimaryFooterButton);
getFooterBarMixin().setSecondaryButton(mSecondaryFooterButton, true /* usePrimaryStyle */);
if (mViewModel.canAssumeUdfps()) {
mFooterMessage6.setVisibility(View.VISIBLE);
mIconShield.setVisibility(View.VISIBLE);
} else {
mFooterMessage6.setVisibility(View.GONE);
mIconShield.setVisibility(View.GONE);
}
mSecondaryFooterButton.setText(getContext(),
mViewModel.getEnrollmentRequest().isAfterSuwOrSuwSuggestedAction()
? R.string.security_settings_fingerprint_enroll_introduction_cancel
: R.string.security_settings_fingerprint_enroll_introduction_no_thanks);
if (mViewModel.isBiometricUnlockDisabledByAdmin()
&& !mViewModel.isParentalConsentRequired()) {
setHeaderText(
getActivity(),
R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled
);
getLayout().setDescriptionText(getDescriptionDisabledByAdmin(context));
} else {
setHeaderText(getActivity(),
R.string.security_settings_fingerprint_enroll_introduction_title);
}
mViewModel.getPageStatusLiveData().observe(this, this::updateFooterButtons);
final RequireScrollMixin requireScrollMixin = getLayout()
.getMixin(RequireScrollMixin.class);
requireScrollMixin.requireScrollWithButton(getActivity(), mPrimaryFooterButton,
getMoreButtonTextRes(), mViewModel::onNextButtonClick);
requireScrollMixin.setOnRequireScrollStateChangedListener(scrollNeeded -> {
if (!scrollNeeded) {
mViewModel.setHasScrolledToBottom();
}
});
}
@Override
public void onAttach(@NonNull Context context) {
mViewModel = mViewModelProvider.get(FingerprintEnrollIntroViewModel.class);
getLifecycle().addObserver(mViewModel);
super.onAttach(context);
}
@Override
public void onDetach() {
getLifecycle().removeObserver(mViewModel);
super.onDetach();
}
@NonNull
private PorterDuffColorFilter getIconColorFilter(@NonNull Context context) {
if (mIconColorFilter == null) {
mIconColorFilter = new PorterDuffColorFilter(
DynamicColorPalette.getColor(context, ACCENT),
PorterDuff.Mode.SRC_IN);
}
return mIconColorFilter;
}
private GlifLayout getLayout() {
return mView.findViewById(R.id.setup_wizard_layout);
}
@NonNull
private FooterBarMixin getFooterBarMixin() {
final GlifLayout layout = getLayout();
return layout.getMixin(FooterBarMixin.class);
}
@NonNull
private String getDescriptionDisabledByAdmin(@NonNull Context context) {
final int defaultStrId =
R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled;
if (mDevicePolicyMgrRes == null) {
Log.w(TAG, "getDescriptionDisabledByAdmin, null device policy manager res");
return "";
}
return mDevicePolicyMgrRes.getString(FINGERPRINT_UNLOCK_DISABLED,
() -> context.getString(defaultStrId));
}
private void setHeaderText(@NonNull Activity activity, int resId) {
TextView layoutTitle = getLayout().getHeaderTextView();
CharSequence previousTitle = layoutTitle.getText();
CharSequence title = activity.getText(resId);
if (previousTitle != title) {
if (!TextUtils.isEmpty(previousTitle)) {
layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
}
getLayout().setHeaderText(title);
getLayout().getHeaderTextView().setContentDescription(title);
activity.setTitle(title);
}
getLayout().getHeaderTextView().setContentDescription(activity.getText(resId));
}
void updateFooterButtons(@NonNull FingerprintEnrollIntroStatus status) {
@StringRes final int scrollToBottomPrimaryResId =
status.getEnrollableStatus() == FINGERPRINT_ENROLLABLE_OK
? R.string.security_settings_fingerprint_enroll_introduction_agree
: R.string.done;
mPrimaryFooterButton.setText(getContext(),
status.hasScrollToBottom() ? scrollToBottomPrimaryResId : getMoreButtonTextRes());
mSecondaryFooterButton.setVisibility(
status.hasScrollToBottom() ? View.VISIBLE : View.INVISIBLE);
final TextView errorTextView = mView.findViewById(R.id.error_text);
switch (status.getEnrollableStatus()) {
case FINGERPRINT_ENROLLABLE_OK:
errorTextView.setText(null);
errorTextView.setVisibility(View.GONE);
break;
case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
errorTextView.setText(R.string.fingerprint_intro_error_max);
errorTextView.setVisibility(View.VISIBLE);
break;
case FINGERPRINT_ENROLLABLE_UNKNOWN:
// default case, do nothing.
}
}
@StringRes
private int getMoreButtonTextRes() {
return R.string.security_settings_face_enroll_introduction_more;
}
}

View File

@@ -0,0 +1,276 @@
/*
* 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.view;
import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY;
import static com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.lifecycle.viewmodel.MutableCreationExtras;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.biometrics.BiometricEnrollBase;
import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollEnrolling;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.factory.BiometricsFragmentFactory;
import com.android.settings.biometrics2.factory.BiometricsViewModelFactory;
import com.android.settings.biometrics2.ui.model.CredentialModel;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
import com.android.settings.overlay.FeatureFactory;
import com.google.android.setupdesign.util.ThemeHelper;
/**
* Fingerprint enrollment activity implementation
*/
public class FingerprintEnrollmentActivity extends FragmentActivity {
private static final String TAG = "FingerprintEnrollmentActivity";
protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1;
private FingerprintEnrollmentViewModel mViewModel;
private AutoCredentialViewModel mAutoCredentialViewModel;
private ActivityResultLauncher<Intent> mNextActivityLauncher;
private ActivityResultLauncher<Intent> mChooseLockLauncher;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mNextActivityLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
(it) -> mViewModel.onContinueEnrollActivityResult(
it,
mAutoCredentialViewModel.getUserId())
);
mChooseLockLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
(it) -> onChooseOrConfirmLockResult(true, it)
);
ViewModelProvider viewModelProvider = new ViewModelProvider(this);
mViewModel = viewModelProvider.get(FingerprintEnrollmentViewModel.class);
mViewModel.setRequest(new EnrollmentRequest(getIntent(), getApplicationContext()));
mViewModel.setSavedInstanceState(savedInstanceState);
getLifecycle().addObserver(mViewModel);
mAutoCredentialViewModel = viewModelProvider.get(AutoCredentialViewModel.class);
mAutoCredentialViewModel.setCredentialModel(new CredentialModel(getIntent(),
SystemClock.elapsedRealtimeClock()));
getLifecycle().addObserver(mAutoCredentialViewModel);
mViewModel.getSetResultLiveData().observe(this, this::onSetActivityResult);
mAutoCredentialViewModel.getActionLiveData().observe(this, this::onCredentialAction);
// Theme
setTheme(mViewModel.getRequest().getTheme());
ThemeHelper.trySetDynamicColor(this);
getWindow().setStatusBarColor(android.graphics.Color.TRANSPARENT);
// fragment
setContentView(R.layout.biometric_enrollment_container);
final FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.setFragmentFactory(
new BiometricsFragmentFactory(getApplication(), viewModelProvider));
final FingerprintEnrollIntroViewModel fingerprintEnrollIntroViewModel =
viewModelProvider.get(FingerprintEnrollIntroViewModel.class);
fingerprintEnrollIntroViewModel.setEnrollmentRequest(mViewModel.getRequest());
fingerprintEnrollIntroViewModel.setUserId(mAutoCredentialViewModel.getUserId());
fingerprintEnrollIntroViewModel.getActionLiveData().observe(
this, this::observeIntroAction);
final String tag = "FingerprintEnrollIntroFragment";
fragmentManager.beginTransaction()
.setReorderingAllowed(true)
.add(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, tag)
.commit();
}
private void onSetActivityResult(@NonNull ActivityResult result) {
setResult(mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()
? RESULT_CANCELED
: result.getResultCode(),
result.getData());
finish();
}
private void onCredentialAction(@NonNull Integer action) {
switch (action) {
case CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK: {
final Intent intent = mAutoCredentialViewModel.getChooseLockIntent(this,
mViewModel.getRequest().isSuw(), mViewModel.getRequest().getSuwExtras());
if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
Log.w(TAG, "chooseLock, fail to set isWaiting flag to true");
}
mChooseLockLauncher.launch(intent);
return;
}
case CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK: {
final boolean launched = mAutoCredentialViewModel.getConfirmLockLauncher(
this,
LAUNCH_CONFIRM_LOCK_ACTIVITY,
getString(R.string.security_settings_fingerprint_preference_title)
).launch();
if (!launched) {
// This shouldn't happen, as we should only end up at this step if a lock thingy
// is already set.
Log.e(TAG, "confirmLock, launched is true");
finish();
} else if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
Log.w(TAG, "confirmLock, fail to set isWaiting flag to true");
}
return;
}
case CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE: {
Log.w(TAG, "observeCredentialLiveData, finish with action:" + action);
if (mViewModel.getRequest().isAfterSuwOrSuwSuggestedAction()) {
setResult(Activity.RESULT_CANCELED);
}
finish();
}
}
}
private void onChooseOrConfirmLockResult(boolean isChooseLock,
@NonNull ActivityResult activityResult) {
if (!mViewModel.isWaitingActivityResult().compareAndSet(true, false)) {
Log.w(TAG, "isChooseLock:" + isChooseLock + ", fail to unset waiting flag");
}
if (mAutoCredentialViewModel.checkNewCredentialFromActivityResult(
isChooseLock, activityResult)) {
overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
}
}
private void observeIntroAction(@NonNull Integer action) {
switch (action) {
case FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH: {
onSetActivityResult(
new ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null));
return;
}
case FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL: {
onSetActivityResult(
new ActivityResult(BiometricEnrollBase.RESULT_SKIP, null));
return;
}
case FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL: {
final boolean isSuw = mViewModel.getRequest().isSuw();
if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) {
Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag");
}
final Intent intent = new Intent(this, isSuw
? SetupFingerprintEnrollEnrolling.class
: FingerprintEnrollFindSensor.class);
intent.putExtras(mAutoCredentialViewModel.getCredentialBundle());
intent.putExtras(mViewModel.getNextActivityBaseIntentExtras());
mNextActivityLauncher.launch(intent);
}
}
}
@Override
protected void onPause() {
super.onPause();
mViewModel.checkFinishActivityDuringOnPause(isFinishing(), isChangingConfigurations());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) {
onChooseOrConfirmLockResult(false, new ActivityResult(resultCode, data));
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
@NonNull
@Override
public CreationExtras getDefaultViewModelCreationExtras() {
final Application application =
super.getDefaultViewModelCreationExtras().get(APPLICATION_KEY);
final MutableCreationExtras ret = new MutableCreationExtras();
ret.set(APPLICATION_KEY, application);
final FingerprintRepository repository = FeatureFactory.getFactory(application)
.getBiometricsRepositoryProvider().getFingerprintRepository(application);
ret.set(CHALLENGE_GENERATOR, new FingerprintChallengeGenerator(repository));
return ret;
}
@NonNull
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return new BiometricsViewModelFactory();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
getWindow().setStatusBarColor(getBackgroundColor());
}
@ColorInt
private int getBackgroundColor() {
final ColorStateList stateList = Utils.getColorAttr(this, android.R.attr.windowBackground);
return stateList != null ? stateList.getDefaultColor() : Color.TRANSPARENT;
}
@Override
protected void onDestroy() {
getLifecycle().removeObserver(mViewModel);
super.onDestroy();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
mViewModel.onSaveInstanceState(outState);
}
}

View File

@@ -0,0 +1,354 @@
/*
* 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.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE;
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN;
import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE;
import android.annotation.IntDef;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.model.CredentialModel;
import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockPattern;
import com.android.settings.password.ChooseLockSettingsHelper;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like
* start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing.
*/
public class AutoCredentialViewModel extends AndroidViewModel implements DefaultLifecycleObserver {
private static final String TAG = "AutoCredentialViewModel";
private static final boolean DEBUG = true;
/**
* Need activity to run choose lock
*/
public static final int CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK = 1;
/**
* Need activity to run confirm lock
*/
public static final int CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK = 2;
/**
* Fail to use challenge from hardware generateChallenge(), shall finish activity with proper
* error code
*/
public static final int CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE = 3;
@IntDef(prefix = { "CREDENTIAL_" }, value = {
CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK,
CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK,
CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE
})
@Retention(RetentionPolicy.SOURCE)
public @interface CredentialAction {}
/**
* Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge
*/
public interface GenerateChallengeCallback {
/**
* Generic generateChallenge method for FingerprintManager or FaceManager
*/
void onChallengeGenerated(int sensorId, int userId, long challenge);
}
/**
* A generic interface class for calling different generateChallenge from FingerprintManager or
* FaceManager
*/
public interface ChallengeGenerator {
/**
* Get callback that will be called later after challenge generated
*/
@Nullable
GenerateChallengeCallback getCallback();
/**
* Set callback that will be called later after challenge generated
*/
void setCallback(@Nullable GenerateChallengeCallback callback);
/**
* Method for generating challenge from FingerprintManager or FaceManager
*/
void generateChallenge(int userId);
}
/**
* Used to generate challenge through FingerprintRepository
*/
public static class FingerprintChallengeGenerator implements ChallengeGenerator {
private static final String TAG = "FingerprintChallengeGenerator";
@NonNull
private final FingerprintRepository mFingerprintRepository;
@Nullable
private GenerateChallengeCallback mCallback = null;
public FingerprintChallengeGenerator(@NonNull FingerprintRepository fingerprintRepository) {
mFingerprintRepository = fingerprintRepository;
}
@Nullable
@Override
public GenerateChallengeCallback getCallback() {
return mCallback;
}
@Override
public void setCallback(@Nullable GenerateChallengeCallback callback) {
mCallback = callback;
}
@Override
public void generateChallenge(int userId) {
final GenerateChallengeCallback callback = mCallback;
if (callback == null) {
Log.e(TAG, "generateChallenge, null callback");
return;
}
mFingerprintRepository.generateChallenge(userId, callback::onChallengeGenerated);
}
}
@NonNull private final LockPatternUtils mLockPatternUtils;
@NonNull private final ChallengeGenerator mChallengeGenerator;
private CredentialModel mCredentialModel = null;
@NonNull private final MutableLiveData<Integer> mActionLiveData =
new MutableLiveData<>();
public AutoCredentialViewModel(
@NonNull Application application,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull ChallengeGenerator challengeGenerator) {
super(application);
mLockPatternUtils = lockPatternUtils;
mChallengeGenerator = challengeGenerator;
}
public void setCredentialModel(@NonNull CredentialModel credentialModel) {
mCredentialModel = credentialModel;
}
/**
* Observe ActionLiveData for actions about choosing lock, confirming lock, or finishing
* activity
*/
@NonNull
public LiveData<Integer> getActionLiveData() {
return mActionLiveData;
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
checkCredential();
}
/**
* Check credential status for biometric enrollment.
*/
private void checkCredential() {
if (isValidCredential()) {
return;
}
final long gkPwHandle = mCredentialModel.getGkPwHandle();
if (isUnspecifiedPassword()) {
mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
} else if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
generateChallenge(gkPwHandle);
} else {
mActionLiveData.postValue(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
}
}
private void generateChallenge(long gkPwHandle) {
mChallengeGenerator.setCallback((sensorId, userId, challenge) -> {
mCredentialModel.setSensorId(sensorId);
mCredentialModel.setChallenge(challenge);
try {
final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId);
mCredentialModel.setToken(newToken);
} catch (IllegalStateException e) {
Log.e(TAG, "generateChallenge, IllegalStateException", e);
mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
return;
}
mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle);
mCredentialModel.clearGkPwHandle();
if (DEBUG) {
Log.d(TAG, "generateChallenge " + mCredentialModel);
}
// Check credential again
if (!isValidCredential()) {
Log.w(TAG, "generateChallenge, invalid Credential");
mActionLiveData.postValue(CREDENTIAL_FAIL_DURING_GENERATE_CHALLENGE);
}
});
mChallengeGenerator.generateChallenge(getUserId());
}
private boolean isValidCredential() {
return !isUnspecifiedPassword()
&& CredentialModel.isValidToken(mCredentialModel.getToken());
}
private boolean isUnspecifiedPassword() {
return mLockPatternUtils.getActivePasswordQuality(getUserId())
== PASSWORD_QUALITY_UNSPECIFIED;
}
/**
* Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern
* @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is
* coming from ConfirmLockPassword or ConfirmLockPattern
* @param result activity result
* @return if it is a valid result
*/
public boolean checkNewCredentialFromActivityResult(boolean isChooseLock,
@NonNull ActivityResult result) {
if ((isChooseLock && result.getResultCode() == ChooseLockPattern.RESULT_FINISHED)
|| (!isChooseLock && result.getResultCode() == Activity.RESULT_OK)) {
final Intent data = result.getData();
if (data != null) {
final long gkPwHandle = result.getData().getLongExtra(
EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE);
generateChallenge(gkPwHandle);
return true;
}
}
return false;
}
/**
* Get userId for this credential
*/
public int getUserId() {
return mCredentialModel.getUserId();
}
@Nullable
private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId)
throws IllegalStateException {
final VerifyCredentialResponse response = mLockPatternUtils
.verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId);
if (!response.isMatched()) {
throw new IllegalStateException("Unable to request Gatekeeper HAT");
}
return response.getGatekeeperHAT();
}
/**
* Get Credential bundle which will be used to launch next activity.
*/
@NonNull
public Bundle getCredentialBundle() {
final Bundle retBundle = new Bundle();
final long gkPwHandle = mCredentialModel.getGkPwHandle();
if (CredentialModel.isValidGkPwHandle(gkPwHandle)) {
retBundle.putLong(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
}
final byte[] token = mCredentialModel.getToken();
if (CredentialModel.isValidToken(token)) {
retBundle.putByteArray(EXTRA_KEY_CHALLENGE_TOKEN, token);
}
final int userId = getUserId();
if (CredentialModel.isValidUserId(userId)) {
retBundle.putInt(Intent.EXTRA_USER_ID, userId);
}
retBundle.putLong(EXTRA_KEY_CHALLENGE, mCredentialModel.getChallenge());
retBundle.putInt(EXTRA_KEY_SENSOR_ID, mCredentialModel.getSensorId());
return retBundle;
}
/**
* Get Intent for choosing lock
*/
@NonNull
public Intent getChooseLockIntent(@NonNull Context context, boolean isSuw,
@NonNull Bundle suwExtras) {
final Intent intent = BiometricUtils.getChooseLockIntent(context, isSuw,
suwExtras);
intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS,
true);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true);
final int userId = getUserId();
if (CredentialModel.isValidUserId(userId)) {
intent.putExtra(Intent.EXTRA_USER_ID, userId);
}
return intent;
}
/**
* Get ConfirmLockLauncher
*/
@NonNull
public ChooseLockSettingsHelper getConfirmLockLauncher(@NonNull Activity activity,
int requestCode, @NonNull String title) {
final ChooseLockSettingsHelper.Builder builder =
new ChooseLockSettingsHelper.Builder(activity);
builder.setRequestCode(requestCode)
.setTitle(title)
.setRequestGatekeeperPasswordHandle(true)
.setForegroundOnly(true)
.setReturnCredentials(true);
final int userId = mCredentialModel.getUserId();
if (CredentialModel.isValidUserId(userId)) {
builder.setUserId(userId);
}
return builder.build();
}
}

View File

@@ -0,0 +1,211 @@
/*
* 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.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
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;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Fingerprint intro onboarding page view model implementation
*/
public class FingerprintEnrollIntroViewModel extends AndroidViewModel
implements DefaultLifecycleObserver {
private static final String TAG = "FingerprintEnrollIntroViewModel";
private static final boolean HAS_SCROLLED_TO_BOTTOM_DEFAULT = false;
private static final int ENROLLABLE_STATUS_DEFAULT = FINGERPRINT_ENROLLABLE_UNKNOWN;
/**
* User clicks 'Done' button on this page
*/
public static final int FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH = 0;
/**
* User clicks 'Agree' button on this page
*/
public static final int FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL = 1;
/**
* User clicks 'Skip' button on this page
*/
public static final int FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL = 2;
@IntDef(prefix = { "FINGERPRINT_ENROLL_INTRO_ACTION_" }, value = {
FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH,
FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL,
FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
})
@Retention(RetentionPolicy.SOURCE)
public @interface FingerprintEnrollIntroAction {}
@NonNull private final FingerprintRepository mFingerprintRepository;
private final MutableLiveData<Boolean> mHasScrolledToBottomLiveData =
new MutableLiveData<>(HAS_SCROLLED_TO_BOTTOM_DEFAULT);
private final MutableLiveData<Integer> mEnrollableStatusLiveData =
new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT);
private final MediatorLiveData<FingerprintEnrollIntroStatus> mPageStatusLiveData =
new MediatorLiveData<>();
private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
private int mUserId = UserHandle.myUserId();
private EnrollmentRequest mEnrollmentRequest = null;
public FingerprintEnrollIntroViewModel(@NonNull Application application,
@NonNull FingerprintRepository fingerprintRepository) {
super(application);
mFingerprintRepository = fingerprintRepository;
mPageStatusLiveData.addSource(
mEnrollableStatusLiveData,
enrollable -> {
final Boolean toBottomValue = mHasScrolledToBottomLiveData.getValue();
final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
toBottomValue != null ? toBottomValue : HAS_SCROLLED_TO_BOTTOM_DEFAULT,
enrollable);
mPageStatusLiveData.setValue(status);
});
mPageStatusLiveData.addSource(
mHasScrolledToBottomLiveData,
hasScrolledToBottom -> {
final Integer enrollableValue = mEnrollableStatusLiveData.getValue();
final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
hasScrolledToBottom,
enrollableValue != null ? enrollableValue : ENROLLABLE_STATUS_DEFAULT);
mPageStatusLiveData.setValue(status);
});
}
public void setUserId(int userId) {
mUserId = userId;
}
public void setEnrollmentRequest(@NonNull EnrollmentRequest enrollmentRequest) {
mEnrollmentRequest = enrollmentRequest;
}
/**
* Get enrollment request
*/
public EnrollmentRequest getEnrollmentRequest() {
return mEnrollmentRequest;
}
private void updateEnrollableStatus() {
final int num = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
final int max =
mEnrollmentRequest.isSuw() && !mEnrollmentRequest.isAfterSuwOrSuwSuggestedAction()
? mFingerprintRepository.getMaxFingerprintsInSuw(getApplication().getResources())
: mFingerprintRepository.getMaxFingerprints();
mEnrollableStatusLiveData.postValue(num >= max
? FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
: FINGERPRINT_ENROLLABLE_OK);
}
/**
* Get enrollable status and hasScrollToBottom live data
*/
public LiveData<FingerprintEnrollIntroStatus> getPageStatusLiveData() {
return mPageStatusLiveData;
}
/**
* Get user's action live data (like clicking Agree, Skip, or Done)
*/
public LiveData<Integer> getActionLiveData() {
return mActionLiveData;
}
/**
* The first sensor type is UDFPS sensor or not
*/
public boolean canAssumeUdfps() {
return mFingerprintRepository.canAssumeUdfps();
}
/**
* Update onboarding intro page has scrolled to bottom
*/
public void setHasScrolledToBottom() {
mHasScrolledToBottomLiveData.postValue(true);
}
/**
* Get parental consent required or not during enrollment process
*/
public boolean isParentalConsentRequired() {
return mFingerprintRepository.isParentalConsentRequired(getApplication());
}
/**
* Get fingerprint is disable by admin or not
*/
public boolean isBiometricUnlockDisabledByAdmin() {
return mFingerprintRepository.isDisabledByAdmin(getApplication(), mUserId);
}
/**
* User clicks next button
*/
public void onNextButtonClick(View ignoredView) {
final Integer status = mEnrollableStatusLiveData.getValue();
switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) {
case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
break;
case FINGERPRINT_ENROLLABLE_OK:
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
break;
default:
Log.w(TAG, "fail to click next, enrolled:" + status);
}
}
/**
* User clicks skip/cancel button
*/
public void onSkipOrCancelButtonClick(View ignoredView) {
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
updateEnrollableStatus();
}
}

View File

@@ -0,0 +1,185 @@
/*
* 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.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT;
import android.app.Application;
import android.app.KeyguardManager;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import com.android.settings.biometrics.BiometricEnrollBase;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
import com.android.settings.password.SetupSkipDialog;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Fingerprint enrollment view model implementation
*/
public class FingerprintEnrollmentViewModel extends AndroidViewModel implements
DefaultLifecycleObserver {
private static final String TAG = "FingerprintEnrollmentViewModel";
@VisibleForTesting
static final String SAVED_STATE_IS_WAITING_ACTIVITY_RESULT = "is_waiting_activity_result";
@NonNull private final FingerprintRepository mFingerprintRepository;
@Nullable private final KeyguardManager mKeyguardManager;
private final AtomicBoolean mIsWaitingActivityResult = new AtomicBoolean(false);
private final MutableLiveData<ActivityResult> mSetResultLiveData = new MutableLiveData<>();
/**
* Even this variable may be nullable, but activity will call setIntent() immediately during
* its onCreate(), we do not assign @Nullable for this variable here.
*/
private EnrollmentRequest mRequest = null;
public FingerprintEnrollmentViewModel(
@NonNull Application application,
@NonNull FingerprintRepository fingerprintRepository,
@Nullable KeyguardManager keyguardManager) {
super(application);
mFingerprintRepository = fingerprintRepository;
mKeyguardManager = keyguardManager;
}
/**
* Set EnrollmentRequest
*/
public void setRequest(@NonNull EnrollmentRequest request) {
mRequest = request;
}
/**
* Get EnrollmentRequest
*/
public EnrollmentRequest getRequest() {
return mRequest;
}
/**
* Copy necessary extra data from activity intent
*/
@NonNull
public Bundle getNextActivityBaseIntentExtras() {
final Bundle bundle = mRequest.getSuwExtras();
bundle.putBoolean(EXTRA_FROM_SETTINGS_SUMMARY, mRequest.isFromSettingsSummery());
return bundle;
}
/**
* Handle activity result from FingerprintFindSensor
*/
public void onContinueEnrollActivityResult(@NonNull ActivityResult result, int userId) {
if (mIsWaitingActivityResult.compareAndSet(true, false)) {
Log.w(TAG, "fail to reset isWaiting flag for enrollment");
}
if (result.getResultCode() == RESULT_FINISHED
|| result.getResultCode() == RESULT_TIMEOUT) {
Intent data = result.getData();
if (mRequest.isSuw() && isKeyguardSecure()
&& result.getResultCode() == RESULT_FINISHED) {
if (data == null) {
data = new Intent();
}
data.putExtras(getSuwFingerprintCountExtra(userId));
}
mSetResultLiveData.postValue(new ActivityResult(result.getResultCode(), data));
} else if (result.getResultCode() == RESULT_SKIP
|| result.getResultCode() == SetupSkipDialog.RESULT_SKIP) {
mSetResultLiveData.postValue(result);
}
}
private boolean isKeyguardSecure() {
return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure();
}
/**
* Activity calls this method during onPause() to finish itself when back to background.
*
* @param isActivityFinishing Activity has called finish() or not
* @param isChangingConfigurations Activity is finished because of configuration changed or not.
*/
public void checkFinishActivityDuringOnPause(boolean isActivityFinishing,
boolean isChangingConfigurations) {
if (isChangingConfigurations || isActivityFinishing || mRequest.isSuw()
|| isWaitingActivityResult().get()) {
return;
}
mSetResultLiveData.postValue(new ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null));
}
@NonNull
private Bundle getSuwFingerprintCountExtra(int userId) {
final Bundle bundle = new Bundle();
bundle.putInt(EXTRA_FINGERPRINT_ENROLLED_COUNT,
mFingerprintRepository.getNumOfEnrolledFingerprintsSize(userId));
return bundle;
}
@NonNull
public LiveData<ActivityResult> getSetResultLiveData() {
return mSetResultLiveData;
}
@NonNull
public AtomicBoolean isWaitingActivityResult() {
return mIsWaitingActivityResult;
}
/**
* Handle savedInstanceState from activity onCreated()
*/
public void setSavedInstanceState(@Nullable Bundle savedInstanceState) {
if (savedInstanceState == null) {
return;
}
mIsWaitingActivityResult.set(
savedInstanceState.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false)
);
}
/**
* Handle onSaveInstanceState from activity
*/
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get());
}
}

View File

@@ -29,6 +29,7 @@ import com.android.settings.accounts.AccountFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider; import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider; import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -161,6 +162,11 @@ public abstract class FeatureFactory {
public abstract FaceFeatureProvider getFaceFeatureProvider(); public abstract FaceFeatureProvider getFaceFeatureProvider();
/**
* Gets implementation for Biometrics repository provider.
*/
public abstract BiometricsRepositoryProvider getBiometricsRepositoryProvider();
/** /**
* Gets implementation for the WifiTrackerLib. * Gets implementation for the WifiTrackerLib.
*/ */

View File

@@ -37,6 +37,8 @@ import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.aware.AwareFeatureProviderImpl; import com.android.settings.aware.AwareFeatureProviderImpl;
import com.android.settings.biometrics.face.FaceFeatureProvider; import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProviderImpl; import com.android.settings.biometrics.face.FaceFeatureProviderImpl;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl;
import com.android.settings.bluetooth.BluetoothFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl; import com.android.settings.bluetooth.BluetoothFeatureProviderImpl;
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl; import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl;
@@ -104,6 +106,7 @@ public class FeatureFactoryImpl extends FeatureFactory {
private BluetoothFeatureProvider mBluetoothFeatureProvider; private BluetoothFeatureProvider mBluetoothFeatureProvider;
private AwareFeatureProvider mAwareFeatureProvider; private AwareFeatureProvider mAwareFeatureProvider;
private FaceFeatureProvider mFaceFeatureProvider; private FaceFeatureProvider mFaceFeatureProvider;
private BiometricsRepositoryProvider mBiometricsRepositoryProvider;
private WifiTrackerLibProvider mWifiTrackerLibProvider; private WifiTrackerLibProvider mWifiTrackerLibProvider;
private SecuritySettingsFeatureProvider mSecuritySettingsFeatureProvider; private SecuritySettingsFeatureProvider mSecuritySettingsFeatureProvider;
private AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider; private AccessibilitySearchFeatureProvider mAccessibilitySearchFeatureProvider;
@@ -305,6 +308,14 @@ public class FeatureFactoryImpl extends FeatureFactory {
return mFaceFeatureProvider; return mFaceFeatureProvider;
} }
@Override
public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
if (mBiometricsRepositoryProvider == null) {
mBiometricsRepositoryProvider = new BiometricsRepositoryProviderImpl();
}
return mBiometricsRepositoryProvider;
}
@Override @Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() { public WifiTrackerLibProvider getWifiTrackerLibProvider() {
if (mWifiTrackerLibProvider == null) { if (mWifiTrackerLibProvider == null) {

View File

@@ -27,6 +27,7 @@ import com.android.settings.accounts.AccountFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider; import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider; import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -78,6 +79,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public final BluetoothFeatureProvider mBluetoothFeatureProvider; public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider; public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider; public final FaceFeatureProvider mFaceFeatureProvider;
public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
public PanelFeatureProvider panelFeatureProvider; public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider; public SlicesFeatureProvider slicesFeatureProvider;
@@ -134,6 +136,7 @@ public class FakeFeatureFactory extends FeatureFactory {
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class); mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
mAwareFeatureProvider = mock(AwareFeatureProvider.class); mAwareFeatureProvider = mock(AwareFeatureProvider.class);
mFaceFeatureProvider = mock(FaceFeatureProvider.class); mFaceFeatureProvider = mock(FaceFeatureProvider.class);
mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class); wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class); securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class); mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -256,6 +259,11 @@ public class FakeFeatureFactory extends FeatureFactory {
return mFaceFeatureProvider; return mFaceFeatureProvider;
} }
@Override
public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
return mBiometricsRepositoryProvider;
}
@Override @Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() { public WifiTrackerLibProvider getWifiTrackerLibProvider() {
return wifiTrackerLibProvider; return wifiTrackerLibProvider;

View File

@@ -23,6 +23,7 @@ import com.android.settings.accounts.AccountFeatureProvider
import com.android.settings.applications.ApplicationFeatureProvider import com.android.settings.applications.ApplicationFeatureProvider
import com.android.settings.aware.AwareFeatureProvider import com.android.settings.aware.AwareFeatureProvider
import com.android.settings.biometrics.face.FaceFeatureProvider import com.android.settings.biometrics.face.FaceFeatureProvider
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
import com.android.settings.bluetooth.BluetoothFeatureProvider import com.android.settings.bluetooth.BluetoothFeatureProvider
import com.android.settings.dashboard.DashboardFeatureProvider import com.android.settings.dashboard.DashboardFeatureProvider
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider
@@ -153,6 +154,10 @@ class FakeFeatureFactory : FeatureFactory() {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun getBiometricsRepositoryProvider(): BiometricsRepositoryProvider {
TODO("Not yet implemented")
}
override fun getWifiTrackerLibProvider(): WifiTrackerLibProvider { override fun getWifiTrackerLibProvider(): WifiTrackerLibProvider {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View File

@@ -0,0 +1,125 @@
/*
* 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.data.repository;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_HOME_BUTTON;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UNKNOWN;
import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.fingerprint.FingerprintManager;
import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.testutils.ResourcesUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(AndroidJUnit4.class)
public class FingerprintRepositoryTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Mock private Resources mResources;
@Mock private FingerprintManager mFingerprintManager;
private Context mContext;
private FingerprintRepository mFingerprintRepository;
@Before
public void setUp() {
mContext = ApplicationProvider.getApplicationContext();
mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
}
@Test
public void testCanAssumeSensorType() {
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 1);
assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 1);
assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue();
setupFingerprintFirstSensor(mFingerprintManager, TYPE_POWER_BUTTON, 1);
assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
setupFingerprintFirstSensor(mFingerprintManager, TYPE_HOME_BUTTON, 1);
assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse();
}
@Test
public void testGetMaxFingerprints() {
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 44);
assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(44);
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 999);
assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(999);
}
@Test
public void testGetNumOfEnrolledFingerprintsSize() {
setupFingerprintEnrolledFingerprints(mFingerprintManager, 10, 3);
setupFingerprintEnrolledFingerprints(mFingerprintManager, 22, 99);
assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(10)).isEqualTo(3);
assertThat(mFingerprintRepository.getNumOfEnrolledFingerprintsSize(22)).isEqualTo(99);
}
@Test
public void testGetMaxFingerprintsInSuw() {
setupSuwMaxFingerprintsEnrollable(mContext, mResources, 333);
assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources))
.isEqualTo(333);
setupSuwMaxFingerprintsEnrollable(mContext, mResources, 20);
assertThat(mFingerprintRepository.getMaxFingerprintsInSuw(mResources)).isEqualTo(20);
}
public static void setupSuwMaxFingerprintsEnrollable(
@NonNull Context context,
@NonNull Resources mockedResources,
int numOfFp) {
final int resId = ResourcesUtils.getResourcesId(context, "integer",
"suw_max_fingerprints_enrollable");
when(mockedResources.getInteger(resId)).thenReturn(numOfFp);
}
}

View File

@@ -0,0 +1,380 @@
/*
* 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.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_NUMERIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_SENSOR_ID;
import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_CHALLENGE;
import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_SENSOR_ID;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CredentialAction;
import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.GenerateChallengeCallback;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.app.Activity;
import android.content.Intent;
import android.os.SystemClock;
import android.os.UserHandle;
import androidx.activity.result.ActivityResult;
import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.settings.biometrics2.ui.model.CredentialModel;
import com.android.settings.password.ChooseLockPattern;
import com.android.settings.password.ChooseLockSettingsHelper;
import com.android.settings.testutils.InstantTaskExecutorRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(AndroidJUnit4.class)
public class AutoCredentialViewModelTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
@Mock private LifecycleOwner mLifecycleOwner;
@Mock private LockPatternUtils mLockPatternUtils;
private TestChallengeGenerator mChallengeGenerator = null;
private AutoCredentialViewModel mAutoCredentialViewModel;
@Before
public void setUp() {
mChallengeGenerator = new TestChallengeGenerator();
mAutoCredentialViewModel = new AutoCredentialViewModel(
ApplicationProvider.getApplicationContext(),
mLockPatternUtils,
mChallengeGenerator);
}
private CredentialModel newCredentialModel(int userId, long challenge,
@Nullable byte[] token, long gkPwHandle) {
final Intent intent = new Intent();
intent.putExtra(Intent.EXTRA_USER_ID, userId);
intent.putExtra(EXTRA_KEY_SENSOR_ID, 1);
intent.putExtra(EXTRA_KEY_CHALLENGE, challenge);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token);
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
return new CredentialModel(intent, SystemClock.elapsedRealtimeClock());
}
private CredentialModel newValidTokenCredentialModel(int userId) {
return newCredentialModel(userId, 1L, new byte[] { 0 }, 0L);
}
private CredentialModel newInvalidChallengeCredentialModel(int userId) {
return newCredentialModel(userId, INVALID_CHALLENGE, null, 0L);
}
private CredentialModel newGkPwHandleCredentialModel(int userId, long gkPwHandle) {
return newCredentialModel(userId, INVALID_CHALLENGE, null, gkPwHandle);
}
private void verifyNothingHappen() {
assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
}
private void verifyOnlyActionLiveData(@CredentialAction int action) {
final Integer value = mAutoCredentialViewModel.getActionLiveData().getValue();
assertThat(value).isEqualTo(action);
}
private void setupGenerateTokenFlow(long gkPwHandle, int userId, int newSensorId,
long newChallenge) {
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_SOMETHING);
mChallengeGenerator.mUserId = userId;
mChallengeGenerator.mSensorId = newSensorId;
mChallengeGenerator.mChallenge = newChallenge;
when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId))
.thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 }));
}
@Test
public void checkCredential_validCredentialCase() {
final int userId = 99;
mAutoCredentialViewModel.setCredentialModel(newValidTokenCredentialModel(userId));
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_SOMETHING);
// Run credential check
mAutoCredentialViewModel.onCreate(mLifecycleOwner);
verifyNothingHappen();
}
@Test
public void checkCredential_needToChooseLock() {
final int userId = 100;
mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_UNSPECIFIED);
// Run credential check
mAutoCredentialViewModel.onCreate(mLifecycleOwner);
verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK);
}
@Test
public void checkCredential_needToConfirmLockFoSomething() {
final int userId = 101;
mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_SOMETHING);
// Run credential check
mAutoCredentialViewModel.onCreate(mLifecycleOwner);
verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
}
@Test
public void checkCredential_needToConfirmLockForNumeric() {
final int userId = 102;
mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_NUMERIC);
// Run credential check
mAutoCredentialViewModel.onCreate(mLifecycleOwner);
verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
}
@Test
public void checkCredential_needToConfirmLockForAlphabetic() {
final int userId = 103;
mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_ALPHABETIC);
// Run credential check
mAutoCredentialViewModel.onCreate(mLifecycleOwner);
verifyOnlyActionLiveData(CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK);
}
@Test
public void checkCredential_generateChallenge() {
final int userId = 104;
final long gkPwHandle = 1111L;
final CredentialModel credentialModel = newGkPwHandleCredentialModel(userId, gkPwHandle);
mAutoCredentialViewModel.setCredentialModel(credentialModel);
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_SOMETHING);
final int newSensorId = 10;
final long newChallenge = 20L;
setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
// Run credential check
mAutoCredentialViewModel.onCreate(mLifecycleOwner);
assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
}
@Test
public void testGetUserId() {
final int userId = 106;
mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
// Get userId
assertThat(mAutoCredentialViewModel.getUserId()).isEqualTo(userId);
}
@Test
public void testCheckNewCredentialFromActivityResult_invalidChooseLock() {
final int userId = 107;
final long gkPwHandle = 3333L;
mAutoCredentialViewModel.setCredentialModel(
newGkPwHandleCredentialModel(userId, gkPwHandle));
final Intent intent = new Intent();
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
// run checkNewCredentialFromActivityResult()
final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
new ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent));
assertThat(ret).isFalse();
verifyNothingHappen();
}
@Test
public void testCheckNewCredentialFromActivityResult_invalidConfirmLock() {
final int userId = 107;
final long gkPwHandle = 3333L;
mAutoCredentialViewModel.setCredentialModel(
newGkPwHandleCredentialModel(userId, gkPwHandle));
final Intent intent = new Intent();
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
// run checkNewCredentialFromActivityResult()
final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
new ActivityResult(Activity.RESULT_OK + 1, intent));
assertThat(ret).isFalse();
verifyNothingHappen();
}
@Test
public void testCheckNewCredentialFromActivityResult_nullDataChooseLock() {
final int userId = 108;
final long gkPwHandle = 4444L;
mAutoCredentialViewModel.setCredentialModel(
newGkPwHandleCredentialModel(userId, gkPwHandle));
// run checkNewCredentialFromActivityResult()
final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
new ActivityResult(ChooseLockPattern.RESULT_FINISHED, null));
assertThat(ret).isFalse();
verifyNothingHappen();
}
@Test
public void testCheckNewCredentialFromActivityResult_nullDataConfirmLock() {
final int userId = 109;
mAutoCredentialViewModel.setCredentialModel(newInvalidChallengeCredentialModel(userId));
// run checkNewCredentialFromActivityResult()
final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
new ActivityResult(Activity.RESULT_OK, null));
assertThat(ret).isFalse();
verifyNothingHappen();
}
@Test
public void testCheckNewCredentialFromActivityResult_validChooseLock() {
final int userId = 108;
final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
mAutoCredentialViewModel.setCredentialModel(credentialModel);
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_SOMETHING);
final long gkPwHandle = 6666L;
final int newSensorId = 50;
final long newChallenge = 60L;
setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
final Intent intent = new Intent();
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
// Run checkNewCredentialFromActivityResult()
final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(true,
new ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent));
assertThat(ret).isTrue();
assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
}
@Test
public void testCheckNewCredentialFromActivityResult_validConfirmLock() {
final int userId = 109;
final CredentialModel credentialModel = newInvalidChallengeCredentialModel(userId);
mAutoCredentialViewModel.setCredentialModel(credentialModel);
when(mLockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
PASSWORD_QUALITY_SOMETHING);
final long gkPwHandle = 5555L;
final int newSensorId = 80;
final long newChallenge = 90L;
setupGenerateTokenFlow(gkPwHandle, userId, newSensorId, newChallenge);
final Intent intent = new Intent();
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle);
// Run checkNewCredentialFromActivityResult()
final boolean ret = mAutoCredentialViewModel.checkNewCredentialFromActivityResult(false,
new ActivityResult(Activity.RESULT_OK, intent));
assertThat(ret).isTrue();
assertThat(mAutoCredentialViewModel.getActionLiveData().getValue()).isNull();
assertThat(credentialModel.getSensorId()).isEqualTo(newSensorId);
assertThat(credentialModel.getChallenge()).isEqualTo(newChallenge);
assertThat(CredentialModel.isValidToken(credentialModel.getToken())).isTrue();
assertThat(CredentialModel.isValidGkPwHandle(credentialModel.getGkPwHandle())).isFalse();
assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1);
}
public static class TestChallengeGenerator implements ChallengeGenerator {
public int mSensorId = INVALID_SENSOR_ID;
public int mUserId = UserHandle.myUserId();
public long mChallenge = INVALID_CHALLENGE;
public int mCallbackRunCount = 0;
private GenerateChallengeCallback mCallback;
@Nullable
@Override
public GenerateChallengeCallback getCallback() {
return mCallback;
}
@Override
public void setCallback(@Nullable GenerateChallengeCallback callback) {
mCallback = callback;
}
@Override
public void generateChallenge(int userId) {
final GenerateChallengeCallback callback = mCallback;
if (callback == null) {
return;
}
callback.onChallengeGenerated(mSensorId, mUserId, mChallenge);
++mCallbackRunCount;
}
}
private VerifyCredentialResponse newGoodCredential(long gkPwHandle, @NonNull byte[] hat) {
return new VerifyCredentialResponse.Builder()
.setGatekeeperPasswordHandle(gkPwHandle)
.setGatekeeperHAT(hat)
.build();
}
}

View File

@@ -0,0 +1,259 @@
/*
* 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.fingerprint.FingerprintSensorProperties.TYPE_REAR;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC;
import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupSuwMaxFingerprintsEnrollable;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK;
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_UNKNOWN;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwDeferredRequest;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwPortalRequest;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwSuggestedActionFlowRequest;
import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.Application;
import android.content.res.Resources;
import android.hardware.fingerprint.FingerprintManager;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
import com.android.settings.testutils.InstantTaskExecutorRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(AndroidJUnit4.class)
public class FingerprintEnrollIntroViewModelTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
@Mock private Resources mResources;
@Mock private LifecycleOwner mLifecycleOwner;
@Mock private FingerprintManager mFingerprintManager;
private Application mApplication;
private FingerprintRepository mFingerprintRepository;
private FingerprintEnrollIntroViewModel mViewModel;
@Before
public void setUp() {
mApplication = ApplicationProvider.getApplicationContext();
mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
mViewModel = new FingerprintEnrollIntroViewModel(mApplication, mFingerprintRepository);
// MediatorLiveData won't update itself unless observed
mViewModel.getPageStatusLiveData().observeForever(event -> {});
}
@Test
public void testPageStatusLiveDataDefaultValue() {
final FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.hasScrollToBottom()).isFalse();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_UNKNOWN);
}
@Test
public void testGetEnrollmentRequest() {
final EnrollmentRequest request = newAllFalseRequest(mApplication);
mViewModel.setEnrollmentRequest(request);
assertThat(mViewModel.getEnrollmentRequest()).isEqualTo(request);
}
@Test
public void testOnStartToUpdateEnrollableStatus_isSuw() {
final int userId = 44;
mViewModel.setUserId(userId);
mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
mViewModel.onStart(mLifecycleOwner);
FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
mViewModel.onStart(mLifecycleOwner);
status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
}
@Test
public void testOnStartToUpdateEnrollableStatus_isNotSuw() {
testOnStartToUpdateEnrollableStatus(newAllFalseRequest(mApplication));
}
@Test
public void testOnStartToUpdateEnrollableStatus_isSuwDeferred() {
testOnStartToUpdateEnrollableStatus(newIsSuwDeferredRequest(mApplication));
}
@Test
public void testOnStartToUpdateEnrollableStatus_isSuwPortal() {
testOnStartToUpdateEnrollableStatus(newIsSuwPortalRequest(mApplication));
}
@Test
public void testOnStartToUpdateEnrollableStatus_isSuwSuggestedActionFlow() {
testOnStartToUpdateEnrollableStatus(newIsSuwSuggestedActionFlowRequest(mApplication));
}
private void testOnStartToUpdateEnrollableStatus(@NonNull EnrollmentRequest request) {
final int userId = 45;
mViewModel.setUserId(userId);
mViewModel.setEnrollmentRequest(request);
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
mViewModel.onStart(mLifecycleOwner);
FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 5);
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5);
mViewModel.onStart(mLifecycleOwner);
status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
}
@Test
public void textCanAssumeUdfps() {
setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1);
assertThat(mViewModel.canAssumeUdfps()).isEqualTo(true);
setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1);
assertThat(mViewModel.canAssumeUdfps()).isEqualTo(false);
}
@Test
public void testIsParentalConsentRequired() {
// We shall not mock FingerprintRepository, but
// FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
// mock static method
final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(true);
assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(true);
when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(false);
assertThat(mViewModel.isParentalConsentRequired()).isEqualTo(false);
}
@Test
public void testIsBiometricUnlockDisabledByAdmin() {
// We shall not mock FingerprintRepository, but
// FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
// static method
final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
mViewModel = new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository);
final int userId = 33;
mViewModel.setUserId(userId);
when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(true);
assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(true);
when(fingerprintRepository.isDisabledByAdmin(mApplication, userId)).thenReturn(false);
assertThat(mViewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(false);
}
@Test
public void testSetHasScrolledToBottom() {
mViewModel.setHasScrolledToBottom();
FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.hasScrollToBottom()).isEqualTo(true);
}
@Test
public void testOnNextButtonClick_enrollNext() {
final int userId = 46;
mViewModel.setUserId(userId);
mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
// Set latest status to FINGERPRINT_ENROLLABLE_OK
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 0);
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
mViewModel.onStart(mLifecycleOwner);
FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
// Perform click on `next`
mViewModel.onNextButtonClick(null);
assertThat(mViewModel.getActionLiveData().getValue())
.isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
}
@Test
public void testOnNextButtonClick_doneAndFinish() {
final int userId = 46;
mViewModel.setUserId(userId);
mViewModel.setEnrollmentRequest(newIsSuwRequest(mApplication));
// Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 1);
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
mViewModel.onStart(mLifecycleOwner);
FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue();
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
// Perform click on `next`
mViewModel.onNextButtonClick(null);
assertThat(mViewModel.getActionLiveData().getValue())
.isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
}
@Test
public void testOnSkipOrCancelButtonClick() {
mViewModel.onSkipOrCancelButtonClick(null);
assertThat(mViewModel.getActionLiveData().getValue())
.isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
}
}

View File

@@ -0,0 +1,252 @@
/*
* 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.biometrics.BiometricEnrollBase.RESULT_FINISHED;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT;
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel.SAVED_STATE_IS_WAITING_ACTIVITY_RESULT;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest;
import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest;
import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import android.app.Application;
import android.app.KeyguardManager;
import android.content.Intent;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import androidx.activity.result.ActivityResult;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
import com.android.settings.password.SetupSkipDialog;
import com.android.settings.testutils.InstantTaskExecutorRule;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@RunWith(AndroidJUnit4.class)
public class FingerprintEnrollmentViewModelTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
@Mock private FingerprintManager mFingerprintManager;
@Mock private KeyguardManager mKeyguardManager;
private Application mApplication;
private FingerprintRepository mFingerprintRepository;
private FingerprintEnrollmentViewModel mViewModel;
@Before
public void setUp() {
mApplication = ApplicationProvider.getApplicationContext();
mFingerprintRepository = new FingerprintRepository(mFingerprintManager);
mViewModel = new FingerprintEnrollmentViewModel(mApplication, mFingerprintRepository,
mKeyguardManager);
}
@Test
public void testGetRequest() {
when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
assertThat(mViewModel.getRequest()).isNull();
final EnrollmentRequest request = newAllFalseRequest(mApplication);
mViewModel.setRequest(request);
assertThat(mViewModel.getRequest()).isEqualTo(request);
}
@Test
public void testGetNextActivityBaseIntentExtras() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
assertThat(mViewModel.getNextActivityBaseIntentExtras()).isNotNull();
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelaySkip1Result() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
final ActivityResult result = new ActivityResult(RESULT_SKIP, null);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, 100);
assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelaySkip2Result() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
final ActivityResult result = new ActivityResult(SetupSkipDialog.RESULT_SKIP, null);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, 100);
assertThat(mViewModel.getSetResultLiveData().getValue()).isEqualTo(result);
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelayNullDataTimeoutResult() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, null);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, 100);
final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
assertThat(setResult).isNotNull();
assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
assertThat(setResult.getData()).isEqualTo(result.getData());
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelayWithDataTimeoutResult() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
final Intent intent = new Intent("testAction");
intent.putExtra("testKey", "testValue");
final ActivityResult result = new ActivityResult(RESULT_TIMEOUT, intent);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, 100);
final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
assertThat(setResult).isNotNull();
assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
assertThat(setResult.getData()).isEqualTo(intent);
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResult() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, 100);
final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
assertThat(setResult).isNotNull();
assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
assertThat(setResult.getData()).isEqualTo(result.getData());
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResult() {
mViewModel.setRequest(newAllFalseRequest(mApplication));
final Intent intent = new Intent("testAction");
intent.putExtra("testKey", "testValue");
final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, 100);
final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
assertThat(setResult).isNotNull();
assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
assertThat(setResult.getData()).isEqualTo(intent);
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelayNullDataFinishResultAsNewData() {
when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
final int userId = 111;
final int numOfFp = 4;
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
mViewModel.setRequest(newIsSuwRequest(mApplication));
final ActivityResult result = new ActivityResult(RESULT_FINISHED, null);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, userId);
final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
assertThat(setResult).isNotNull();
assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
assertThat(setResult.getData()).isNotNull();
assertThat(setResult.getData().getExtras()).isNotNull();
assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
.isEqualTo(numOfFp);
}
@Test
public void testOnContinueEnrollActivityResult_shouldRelayWithDataFinishResultAsNewData() {
when(mKeyguardManager.isKeyguardSecure()).thenReturn(true);
final int userId = 20;
final int numOfFp = 9;
setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, numOfFp);
mViewModel.setRequest(newIsSuwRequest(mApplication));
final String action = "testAction";
final String key = "testKey";
final String value = "testValue";
final Intent intent = new Intent(action);
intent.putExtra(key, value);
final ActivityResult result = new ActivityResult(RESULT_FINISHED, intent);
// Run onContinueEnrollActivityResult
mViewModel.onContinueEnrollActivityResult(result, userId);
final ActivityResult setResult = mViewModel.getSetResultLiveData().getValue();
assertThat(setResult).isNotNull();
assertThat(setResult.getResultCode()).isEqualTo(result.getResultCode());
assertThat(setResult.getData()).isNotNull();
assertThat(setResult.getData().getExtras()).isNotNull();
assertThat(setResult.getData().getExtras().getInt(EXTRA_FINGERPRINT_ENROLLED_COUNT, -1))
.isEqualTo(numOfFp);
assertThat(setResult.getData().getExtras().getString(key)).isEqualTo(value);
}
@Test
public void testSetSavedInstanceState() {
final Bundle bundle = new Bundle();
mViewModel.isWaitingActivityResult().set(true);
// setSavedInstanceState() as false
bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, false);
mViewModel.setSavedInstanceState(bundle);
assertThat(mViewModel.isWaitingActivityResult().get()).isFalse();
// setSavedInstanceState() as false
bundle.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, true);
mViewModel.setSavedInstanceState(bundle);
assertThat(mViewModel.isWaitingActivityResult().get()).isTrue();
}
@Test
public void testOnSaveInstanceState() {
final Bundle bundle = new Bundle();
// setSavedInstanceState() as false
mViewModel.isWaitingActivityResult().set(false);
mViewModel.onSaveInstanceState(bundle);
assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isFalse();
// setSavedInstanceState() as false
mViewModel.isWaitingActivityResult().set(true);
mViewModel.onSaveInstanceState(bundle);
assertThat(bundle.getBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT)).isTrue();
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.util;
import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_FIRST_RUN;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_PORTAL_SETUP;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SETUP_FLOW;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW;
import static com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_THEME;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
public class EnrollmentRequestUtil {
@NonNull
public static EnrollmentRequest newAllFalseRequest(@NonNull Context context) {
return newRequest(context, false, false, false, false, false, false, null);
}
@NonNull
public static EnrollmentRequest newIsSuwRequest(@NonNull Context context) {
return newRequest(context, true, false, false, false, false, false, null);
}
@NonNull
public static EnrollmentRequest newIsSuwDeferredRequest(@NonNull Context context) {
return newRequest(context, true, true, false, false, false, false, null);
}
@NonNull
public static EnrollmentRequest newIsSuwPortalRequest(@NonNull Context context) {
return newRequest(context, true, false, true, false, false, false, null);
}
@NonNull
public static EnrollmentRequest newIsSuwSuggestedActionFlowRequest(
@NonNull Context context) {
return newRequest(context, true, false, false, true, false, false, null);
}
@NonNull
public static EnrollmentRequest newRequest(@NonNull Context context, boolean isSuw,
boolean isSuwDeferred, boolean isSuwPortal, boolean isSuwSuggestedActionFlow,
boolean isSuwFirstRun, boolean isFromSettingsSummery, String theme) {
Intent i = new Intent();
i.putExtra(EXTRA_IS_SETUP_FLOW, isSuw);
i.putExtra(EXTRA_IS_DEFERRED_SETUP, isSuwDeferred);
i.putExtra(EXTRA_IS_PORTAL_SETUP, isSuwPortal);
i.putExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, isSuwSuggestedActionFlow);
i.putExtra(EXTRA_IS_FIRST_RUN, isSuwFirstRun);
i.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, isFromSettingsSummery);
if (!TextUtils.isEmpty(theme)) {
i.putExtra(EXTRA_THEME, theme);
}
return new EnrollmentRequest(i, context);
}
}

View File

@@ -0,0 +1,58 @@
/*
* 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.util;
import static org.mockito.Mockito.when;
import android.hardware.biometrics.SensorProperties;
import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorProperties;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import androidx.annotation.NonNull;
import java.util.ArrayList;
public class FingerprintManagerUtil {
public static void setupFingerprintFirstSensor(
@NonNull FingerprintManager mockedFingerprintManager,
@FingerprintSensorProperties.SensorType int sensorType,
int maxEnrollmentsPerUser) {
final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
props.add(new FingerprintSensorPropertiesInternal(
0 /* sensorId */,
SensorProperties.STRENGTH_STRONG,
maxEnrollmentsPerUser,
new ArrayList<>() /* componentInfo */,
sensorType,
true /* resetLockoutRequiresHardwareAuthToken */));
when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props);
}
public static void setupFingerprintEnrolledFingerprints(
@NonNull FingerprintManager mockedFingerprintManager,
int userId,
int enrolledFingerprints) {
final ArrayList<Fingerprint> ret = new ArrayList<>();
for (int i = 0; i < enrolledFingerprints; ++i) {
ret.add(new Fingerprint("name", 0, 0, 0L));
}
when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret);
}
}

View File

@@ -25,6 +25,7 @@ import com.android.settings.accounts.AccountFeatureProvider;
import com.android.settings.applications.ApplicationFeatureProvider; import com.android.settings.applications.ApplicationFeatureProvider;
import com.android.settings.aware.AwareFeatureProvider; import com.android.settings.aware.AwareFeatureProvider;
import com.android.settings.biometrics.face.FaceFeatureProvider; import com.android.settings.biometrics.face.FaceFeatureProvider;
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
import com.android.settings.bluetooth.BluetoothFeatureProvider; import com.android.settings.bluetooth.BluetoothFeatureProvider;
import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.dashboard.DashboardFeatureProvider;
import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider; import com.android.settings.dashboard.suggestions.SuggestionFeatureProvider;
@@ -73,6 +74,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public final BluetoothFeatureProvider mBluetoothFeatureProvider; public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider; public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider; public final FaceFeatureProvider mFaceFeatureProvider;
public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
public PanelFeatureProvider panelFeatureProvider; public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider; public SlicesFeatureProvider slicesFeatureProvider;
@@ -120,6 +122,7 @@ public class FakeFeatureFactory extends FeatureFactory {
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class); mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
mAwareFeatureProvider = mock(AwareFeatureProvider.class); mAwareFeatureProvider = mock(AwareFeatureProvider.class);
mFaceFeatureProvider = mock(FaceFeatureProvider.class); mFaceFeatureProvider = mock(FaceFeatureProvider.class);
mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class); wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class); securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class); mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
@@ -242,6 +245,11 @@ public class FakeFeatureFactory extends FeatureFactory {
return mFaceFeatureProvider; return mFaceFeatureProvider;
} }
@Override
public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
return mBiometricsRepositoryProvider;
}
@Override @Override
public WifiTrackerLibProvider getWifiTrackerLibProvider() { public WifiTrackerLibProvider getWifiTrackerLibProvider() {
return wifiTrackerLibProvider; return wifiTrackerLibProvider;

View File

@@ -0,0 +1,59 @@
/*
* 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.testutils;
import androidx.arch.core.executor.ArchTaskExecutor;
import androidx.arch.core.executor.TaskExecutor;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
/**
* A JUnit Test Rule that swaps the background executor used by the Architecture Components with a
* different one which executes each task synchronously.
*
* We can't refer it in prebuilt androidX library.
* Copied it from androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
*/
public class InstantTaskExecutorRule extends TestWatcher {
@Override
protected void starting(Description description) {
super.starting(description);
ArchTaskExecutor.getInstance().setDelegate(new TaskExecutor() {
@Override
public void executeOnDiskIO(Runnable runnable) {
runnable.run();
}
@Override
public void postToMainThread(Runnable runnable) {
runnable.run();
}
@Override
public boolean isMainThread() {
return true;
}
});
}
@Override
protected void finished(Description description) {
super.finished(description);
ArchTaskExecutor.getInstance().setDelegate(null);
}
}