Merge "Fingerprint Introduction FragmentActivity"

This commit is contained in:
TreeHugger Robot
2022-11-16 00:04:49 +00:00
committed by Android (Google) Code Review
30 changed files with 3319 additions and 1 deletions

View File

@@ -2403,6 +2403,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);
}
}