Merge "Remove package biometric2 from AOSP settings" into main
This commit is contained in:
@@ -2815,20 +2815,6 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".biometrics2.ui.view.FingerprintEnrollmentActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/GlifTheme.Light" />
|
||||
|
||||
<activity android:name=".biometrics2.ui.view.FingerprintEnrollmentActivity$InternalActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/GlifTheme.Light"
|
||||
android:taskAffinity="com.android.settings.root" />
|
||||
|
||||
<activity android:name=".biometrics2.ui.view.FingerprintEnrollmentActivity$SetupActivity"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.MANAGE_FINGERPRINT"
|
||||
android:theme="@style/GlifTheme.Light" />
|
||||
|
||||
<activity android:name=".biometrics.fingerprint.FingerprintEnrollIntroductionInternal"
|
||||
android:exported="false"
|
||||
android:theme="@style/GlifTheme.Light"
|
||||
|
@@ -43,7 +43,6 @@ import com.android.settings.biometrics.face.FaceEnrollIntroductionInternal;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
|
||||
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.homepage.DeepLinkHomepageActivity;
|
||||
import com.android.settings.homepage.DeepLinkHomepageActivityInternal;
|
||||
@@ -255,8 +254,6 @@ public class ActivityEmbeddingRulesController {
|
||||
.buildSearchIntent(mContext, SettingsEnums.SETTINGS_HOMEPAGE);
|
||||
addActivityFilter(activityFilters, searchIntent);
|
||||
}
|
||||
addActivityFilter(activityFilters, FingerprintEnrollmentActivity.class);
|
||||
addActivityFilter(activityFilters, FingerprintEnrollmentActivity.InternalActivity.class);
|
||||
addActivityFilter(activityFilters, FingerprintEnrollIntroduction.class);
|
||||
addActivityFilter(activityFilters, FingerprintEnrollIntroductionInternal.class);
|
||||
addActivityFilter(activityFilters, FingerprintEnrollEnrolling.class);
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
package com.android.settings.biometrics;
|
||||
|
||||
import static android.util.FeatureFlagUtils.SETTINGS_BIOMETRICS2_ENROLLMENT;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.app.Activity;
|
||||
@@ -33,7 +32,6 @@ import android.os.Bundle;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.text.BidiFormatter;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
|
||||
@@ -50,7 +48,6 @@ import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroduction;
|
||||
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor;
|
||||
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction;
|
||||
import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.settings.password.ChooseLockGeneric;
|
||||
import com.android.settings.password.ChooseLockSettingsHelper;
|
||||
@@ -254,17 +251,8 @@ public class BiometricUtils {
|
||||
public static Intent getFingerprintFindSensorIntent(@NonNull Context context,
|
||||
@NonNull Intent activityIntent) {
|
||||
final boolean isSuw = WizardManagerHelper.isAnySetupWizard(activityIntent);
|
||||
final Intent intent;
|
||||
if (FeatureFlagUtils.isEnabled(context, SETTINGS_BIOMETRICS2_ENROLLMENT)) {
|
||||
intent = new Intent(context, isSuw
|
||||
? FingerprintEnrollmentActivity.SetupActivity.class
|
||||
: FingerprintEnrollmentActivity.class);
|
||||
intent.putExtra(BiometricEnrollActivity.EXTRA_SKIP_INTRO, true);
|
||||
} else {
|
||||
intent = new Intent(context, isSuw
|
||||
? SetupFingerprintEnrollFindSensor.class
|
||||
: FingerprintEnrollFindSensor.class);
|
||||
}
|
||||
final Intent intent = new Intent(context, isSuw
|
||||
? SetupFingerprintEnrollFindSensor.class : FingerprintEnrollFindSensor.class);
|
||||
if (isSuw) {
|
||||
SetupWizardUtils.copySetupExtras(activityIntent, intent);
|
||||
}
|
||||
@@ -279,16 +267,8 @@ public class BiometricUtils {
|
||||
public static Intent getFingerprintIntroIntent(@NonNull Context context,
|
||||
@NonNull Intent activityIntent) {
|
||||
final boolean isSuw = WizardManagerHelper.isAnySetupWizard(activityIntent);
|
||||
final Intent intent;
|
||||
if (FeatureFlagUtils.isEnabled(context, SETTINGS_BIOMETRICS2_ENROLLMENT)) {
|
||||
intent = new Intent(context, isSuw
|
||||
? FingerprintEnrollmentActivity.SetupActivity.class
|
||||
: FingerprintEnrollmentActivity.class);
|
||||
} else {
|
||||
intent = new Intent(context, isSuw
|
||||
? SetupFingerprintEnrollIntroduction.class
|
||||
: FingerprintEnrollIntroduction.class);
|
||||
}
|
||||
final Intent intent = new Intent(context, isSuw
|
||||
? SetupFingerprintEnrollIntroduction.class : FingerprintEnrollIntroduction.class);
|
||||
if (isSuw) {
|
||||
WizardManagerHelper.copyWizardManagerExtras(activityIntent, intent);
|
||||
}
|
||||
|
@@ -45,7 +45,6 @@ import android.os.UserManager;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.util.FeatureFlagUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImeAwareEditText;
|
||||
@@ -70,8 +69,6 @@ import com.android.settings.Utils;
|
||||
import com.android.settings.biometrics.BiometricEnrollBase;
|
||||
import com.android.settings.biometrics.BiometricUtils;
|
||||
import com.android.settings.biometrics.GatekeeperPasswordProvider;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
|
||||
import com.android.settings.biometrics2.ui.view.FingerprintEnrollmentActivity;
|
||||
import com.android.settings.core.SettingsBaseActivity;
|
||||
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
|
||||
import com.android.settings.dashboard.DashboardFragment;
|
||||
@@ -833,15 +830,8 @@ public class FingerprintSettings extends SubSettings {
|
||||
if (KEY_FINGERPRINT_ADD.equals(key)) {
|
||||
mIsEnrolling = true;
|
||||
Intent intent = new Intent();
|
||||
if (FeatureFlagUtils.isEnabled(getContext(),
|
||||
FeatureFlagUtils.SETTINGS_BIOMETRICS2_ENROLLMENT)) {
|
||||
intent.setClassName(SETTINGS_PACKAGE_NAME,
|
||||
FingerprintEnrollmentActivity.InternalActivity.class.getName());
|
||||
intent.putExtra(EnrollmentRequest.EXTRA_SKIP_FIND_SENSOR, true);
|
||||
} else {
|
||||
intent.setClassName(SETTINGS_PACKAGE_NAME,
|
||||
FingerprintEnrollEnrolling.class.getName());
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_USER_ID, mUserId);
|
||||
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
|
||||
if (mCalibrator != null) {
|
||||
@@ -1126,12 +1116,7 @@ public class FingerprintSettings extends SubSettings {
|
||||
private void addFirstFingerprint(@Nullable Long gkPwHandle) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClassName(SETTINGS_PACKAGE_NAME,
|
||||
FeatureFlagUtils.isEnabled(getActivity(),
|
||||
FeatureFlagUtils.SETTINGS_BIOMETRICS2_ENROLLMENT)
|
||||
? FingerprintEnrollmentActivity.InternalActivity.class.getName()
|
||||
: FingerprintEnrollIntroductionInternal.class.getName()
|
||||
);
|
||||
|
||||
FingerprintEnrollIntroductionInternal.class.getName());
|
||||
intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true);
|
||||
intent.putExtra(SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
|
||||
SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE);
|
||||
|
@@ -1,4 +0,0 @@
|
||||
# The Android Biometric team should approve all changes to biometrics2 subdirectories.
|
||||
set noparent
|
||||
|
||||
include /src/com/android/settings/biometrics/OWNERS
|
@@ -1,151 +0,0 @@
|
||||
/*
|
||||
* 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 android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
|
||||
import android.util.Log;
|
||||
|
||||
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 {
|
||||
|
||||
private static final String TAG = "FingerprintRepository";
|
||||
@NonNull
|
||||
private final FingerprintManager mFingerprintManager;
|
||||
|
||||
private List<FingerprintSensorPropertiesInternal> mSensorPropertiesCache;
|
||||
|
||||
public FingerprintRepository(@NonNull FingerprintManager fingerprintManager) {
|
||||
mFingerprintManager = fingerprintManager;
|
||||
mFingerprintManager.addAuthenticatorsRegisteredCallback(
|
||||
new IFingerprintAuthenticatorsRegisteredCallback.Stub() {
|
||||
@Override
|
||||
public void onAllAuthenticatorsRegistered(
|
||||
List<FingerprintSensorPropertiesInternal> sensors) {
|
||||
mSensorPropertiesCache = sensors;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The first sensor type is UDFPS sensor or not
|
||||
*/
|
||||
public boolean canAssumeUdfps() {
|
||||
FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
|
||||
return prop != null && prop.isAnyUdfpsType();
|
||||
}
|
||||
|
||||
/**
|
||||
* The first sensor type is Side fps sensor or not
|
||||
*/
|
||||
public boolean canAssumeSfps() {
|
||||
FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal();
|
||||
return prop != null && prop.isAnySidefpsType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first FingerprintSensorPropertiesInternal from FingerprintManager
|
||||
*/
|
||||
@Nullable
|
||||
public FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
|
||||
final List<FingerprintSensorPropertiesInternal> props = mSensorPropertiesCache;
|
||||
if (props == null) {
|
||||
// Handle this case if it really happens
|
||||
Log.e(TAG, "Sensor properties cache is null");
|
||||
return null;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fingerprint enroll stage threshold
|
||||
*/
|
||||
public float getEnrollStageThreshold(int index) {
|
||||
return mFingerprintManager.getEnrollStageThreshold(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fingerprint enroll stage count
|
||||
*/
|
||||
public int getEnrollStageCount() {
|
||||
return mFingerprintManager.getEnrollStageCount();
|
||||
}
|
||||
}
|
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
private static volatile FingerprintRepository sFingerprintRepository;
|
||||
|
||||
/**
|
||||
* Get FingerprintRepository
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public FingerprintRepository getFingerprintRepository(@NonNull Application application) {
|
||||
final FingerprintManager fingerprintManager =
|
||||
Utils.getFingerprintManagerOrNull(application);
|
||||
if (fingerprintManager == null) {
|
||||
return null;
|
||||
}
|
||||
if (sFingerprintRepository == null) {
|
||||
synchronized (FingerprintRepository.class) {
|
||||
if (sFingerprintRepository == null) {
|
||||
sFingerprintRepository = new FingerprintRepository(fingerprintManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sFingerprintRepository;
|
||||
}
|
||||
}
|
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* 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.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.biometrics.fingerprint.FingerprintUpdater;
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
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.ChallengeGenerator;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel;
|
||||
import com.android.settings.overlay.FeatureFactory;
|
||||
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
|
||||
|
||||
/**
|
||||
* View model factory for biometric enrollment fragment
|
||||
*/
|
||||
public class BiometricsViewModelFactory implements ViewModelProvider.Factory {
|
||||
|
||||
private static final String TAG = "BiometricsViewModelFactory";
|
||||
|
||||
public static final CreationExtras.Key<ChallengeGenerator> CHALLENGE_GENERATOR_KEY =
|
||||
new CreationExtras.Key<ChallengeGenerator>() {};
|
||||
public static final CreationExtras.Key<EnrollmentRequest> ENROLLMENT_REQUEST_KEY =
|
||||
new CreationExtras.Key<EnrollmentRequest>() {};
|
||||
public static final CreationExtras.Key<CredentialModel> CREDENTIAL_MODEL_KEY =
|
||||
new CreationExtras.Key<CredentialModel>() {};
|
||||
|
||||
@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.getFeatureFactory();
|
||||
final BiometricsRepositoryProvider provider =
|
||||
featureFactory.getBiometricsRepositoryProvider();
|
||||
|
||||
if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) {
|
||||
final LockPatternUtils lockPatternUtils =
|
||||
featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application);
|
||||
final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR_KEY);
|
||||
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
|
||||
if (challengeGenerator != null && credentialModel != null) {
|
||||
return (T) new AutoCredentialViewModel(application, lockPatternUtils,
|
||||
challengeGenerator, credentialModel);
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) {
|
||||
return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application),
|
||||
application.getMainExecutor());
|
||||
} else if (modelClass.isAssignableFrom(DeviceRotationViewModel.class)) {
|
||||
return (T) new DeviceRotationViewModel(application);
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollFindSensorViewModel.class)) {
|
||||
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
|
||||
if (request != null) {
|
||||
return (T) new FingerprintEnrollFindSensorViewModel(application, request.isSuw());
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) {
|
||||
final FingerprintRepository repository = provider.getFingerprintRepository(application);
|
||||
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
|
||||
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
|
||||
if (repository != null && request != null && credentialModel != null) {
|
||||
return (T) new FingerprintEnrollIntroViewModel(application, repository, request,
|
||||
credentialModel.getUserId());
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollmentViewModel.class)) {
|
||||
final FingerprintRepository repository = provider.getFingerprintRepository(application);
|
||||
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
|
||||
if (repository != null && request != null) {
|
||||
return (T) new FingerprintEnrollmentViewModel(application, repository, request);
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) {
|
||||
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
|
||||
if (credentialModel != null) {
|
||||
return (T) new FingerprintEnrollProgressViewModel(application,
|
||||
new FingerprintUpdater(application), credentialModel.getUserId());
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) {
|
||||
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
|
||||
final FingerprintRepository fingerprint = provider.getFingerprintRepository(
|
||||
application);
|
||||
if (fingerprint != null && credentialModel != null) {
|
||||
return (T) new FingerprintEnrollEnrollingViewModel(application,
|
||||
credentialModel.getUserId(), fingerprint);
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollFinishViewModel.class)) {
|
||||
final CredentialModel credentialModel = extras.get(CREDENTIAL_MODEL_KEY);
|
||||
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
|
||||
final FingerprintRepository fingerprint = provider.getFingerprintRepository(
|
||||
application);
|
||||
if (fingerprint != null && credentialModel != null && request != null) {
|
||||
return (T) new FingerprintEnrollFinishViewModel(application,
|
||||
credentialModel.getUserId(), request, fingerprint);
|
||||
}
|
||||
} else if (modelClass.isAssignableFrom(FingerprintEnrollErrorDialogViewModel.class)) {
|
||||
final EnrollmentRequest request = extras.get(ENROLLMENT_REQUEST_KEY);
|
||||
if (request != null) {
|
||||
return (T) new FingerprintEnrollErrorDialogViewModel(application, request.isSuw());
|
||||
}
|
||||
}
|
||||
return create(modelClass);
|
||||
}
|
||||
}
|
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
import android.content.Intent.EXTRA_USER_ID
|
||||
import android.os.Bundle
|
||||
import android.os.UserHandle
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE
|
||||
import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN
|
||||
import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* Secret credential data including
|
||||
* 1. userId
|
||||
* 2. challenge
|
||||
* 3. token
|
||||
* 4. gkPwHandle
|
||||
*/
|
||||
class CredentialModel(bundle: Bundle?, private val clock: Clock) {
|
||||
|
||||
private val mInitMillis = clock.millis()
|
||||
|
||||
/** userId for this credential */
|
||||
val userId: Int = (bundle ?: Bundle()).getInt(EXTRA_USER_ID, UserHandle.myUserId())
|
||||
|
||||
private var clearGkPwHandleMillis: Long? = null
|
||||
|
||||
/** Gatekeeper password handle */
|
||||
var gkPwHandle: Long = (bundle ?: Bundle()).getLong(EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE)
|
||||
private set
|
||||
|
||||
val isValidGkPwHandle: Boolean
|
||||
get() = gkPwHandle != INVALID_GK_PW_HANDLE
|
||||
|
||||
/** Clear gatekeeper password handle data */
|
||||
fun clearGkPwHandle() {
|
||||
clearGkPwHandleMillis = clock.millis()
|
||||
gkPwHandle = INVALID_GK_PW_HANDLE
|
||||
}
|
||||
|
||||
/** Check user id is valid or not */
|
||||
val isValidUserId: Boolean
|
||||
get() = userId != UserHandle.USER_NULL
|
||||
|
||||
private var updateChallengeMillis: Long? = null
|
||||
|
||||
var challenge: Long = (bundle ?: Bundle()).getLong(EXTRA_KEY_CHALLENGE, INVALID_CHALLENGE)
|
||||
set(value) {
|
||||
updateChallengeMillis = clock.millis()
|
||||
field = value
|
||||
}
|
||||
|
||||
val isValidChallenge: Boolean
|
||||
get() = challenge != INVALID_CHALLENGE
|
||||
|
||||
private var updateTokenMillis: Long? = null
|
||||
|
||||
/** Challenge token */
|
||||
var token: ByteArray? = (bundle ?: Bundle()).getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)
|
||||
set(value) {
|
||||
updateTokenMillis = clock.millis()
|
||||
field = value
|
||||
}
|
||||
|
||||
val isValidToken: Boolean
|
||||
get() = token != null
|
||||
|
||||
/** Returns a string representation of the object */
|
||||
override fun toString(): String {
|
||||
val gkPwHandleLen = "$gkPwHandle".length
|
||||
val tokenLen = token?.size ?: 0
|
||||
val challengeLen = "$challenge".length
|
||||
return (javaClass.simpleName + ":{initMillis:$mInitMillis"
|
||||
+ ", userId:$userId"
|
||||
+ ", challenge:{len:$challengeLen"
|
||||
+ ", updateMillis:$updateChallengeMillis}"
|
||||
+ ", token:{len:$tokenLen, isValid:$isValidToken"
|
||||
+ ", updateMillis:$updateTokenMillis}"
|
||||
+ ", gkPwHandle:{len:$gkPwHandleLen, isValid:$isValidGkPwHandle"
|
||||
+ ", clearMillis:$clearGkPwHandleMillis}"
|
||||
+ " }")
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Default value for an invalid challenge */
|
||||
@VisibleForTesting
|
||||
const val INVALID_CHALLENGE = -1L
|
||||
|
||||
/** Default value if GkPwHandle is invalid */
|
||||
@VisibleForTesting
|
||||
const val INVALID_GK_PW_HANDLE = 0L
|
||||
}
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
/** Biometric Enrollment progress */
|
||||
class EnrollmentProgress(val steps: Int, val remaining: Int) {
|
||||
|
||||
val isInitialStep: Boolean
|
||||
get() = steps == INITIAL_STEPS
|
||||
|
||||
override fun toString(): String {
|
||||
return ("${javaClass.simpleName}@${Integer.toHexString(hashCode())}"
|
||||
+ "{steps:$steps, remaining:$remaining}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INITIAL_STEPS = -1
|
||||
const val INITIAL_REMAINING = 0
|
||||
}
|
||||
}
|
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.android.settings.SetupWizardUtils
|
||||
import com.android.settings.biometrics.BiometricEnrollActivity.EXTRA_SKIP_INTRO
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW
|
||||
|
||||
/**
|
||||
* Biometric enrollment generic intent data, which includes
|
||||
* 1. isSuw
|
||||
* 2. isAfterSuwOrSuwSuggestedAction
|
||||
* 3. theme
|
||||
* 4. isFromSettingsSummery
|
||||
* 5. isSkipIntro
|
||||
* 6. isSkipFindSensor
|
||||
* 7. a helper method, getSetupWizardExtras
|
||||
*/
|
||||
class EnrollmentRequest(
|
||||
intent: Intent,
|
||||
context: Context,
|
||||
isSetupActivity: Boolean
|
||||
) {
|
||||
val isSuw: Boolean = isSetupActivity && WizardManagerHelper.isAnySetupWizard(intent)
|
||||
|
||||
val isAfterSuwOrSuwSuggestedAction = (isSetupActivity
|
||||
&& (WizardManagerHelper.isDeferredSetupWizard(intent)
|
||||
|| WizardManagerHelper.isPortalSetupWizard(intent)
|
||||
|| intent.getBooleanExtra(EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, false)))
|
||||
|
||||
private val _suwExtras = getSuwExtras(isSuw, intent)
|
||||
|
||||
val isSkipIntro = intent.getBooleanExtra(EXTRA_SKIP_INTRO, false)
|
||||
|
||||
val isSkipFindSensor = intent.getBooleanExtra(EXTRA_SKIP_FIND_SENSOR, false)
|
||||
|
||||
val theme = SetupWizardUtils.getTheme(context, intent)
|
||||
|
||||
val suwExtras: Bundle
|
||||
get() = Bundle(_suwExtras)
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return (javaClass.simpleName + ":{isSuw:" + isSuw
|
||||
+ ", isAfterSuwOrSuwSuggestedAction:" + isAfterSuwOrSuwSuggestedAction
|
||||
+ "}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SKIP_FIND_SENSOR = "skip_find_sensor"
|
||||
private fun getSuwExtras(isSuw: Boolean, intent: Intent): Bundle {
|
||||
val toIntent = Intent()
|
||||
if (isSuw) {
|
||||
SetupWizardUtils.copySetupExtras(intent, toIntent)
|
||||
}
|
||||
return toIntent.extras ?: Bundle()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
/** Enrolling status message (help or error) */
|
||||
class EnrollmentStatusMessage(val msgId: Int, string: CharSequence?) {
|
||||
|
||||
/** Status string */
|
||||
val str: CharSequence = string ?: ""
|
||||
|
||||
override fun toString(): String {
|
||||
return "${javaClass.simpleName}@${Integer.toHexString(hashCode())}{id:$msgId, str:$str}"
|
||||
}
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
|
||||
enum class FingerprintEnrollable {
|
||||
// Unconfirmed case, this value is invalid, and view shall bypass this value
|
||||
FINGERPRINT_ENROLLABLE_UNKNOWN,
|
||||
// User is allowed to enrolled a new fingerprint
|
||||
FINGERPRINT_ENROLLABLE_OK,
|
||||
// User is not allowed to enroll because the number has reached maximum
|
||||
FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class FingerprintEnrollIntroStatus(
|
||||
private val mHasScrollToBottom: Boolean,
|
||||
/** Enrollable status. It means that user is allowed to enroll a new fingerprint or not. */
|
||||
val enrollableStatus: FingerprintEnrollable
|
||||
) {
|
||||
/** Get info for this onboarding introduction page has scrolled to bottom or not */
|
||||
fun hasScrollToBottom(): Boolean {
|
||||
return mHasScrollToBottom
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("${javaClass.simpleName}@${Integer.toHexString(hashCode())}"
|
||||
+ "{scrollToBottom:$mHasScrollToBottom"
|
||||
+ ", enrollableStatus:$enrollableStatus}")
|
||||
}
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.android.settings.R
|
||||
|
||||
/**
|
||||
* Icon Touch dialog
|
||||
*/
|
||||
class FingerprintEnrollEnrollingIconTouchDialog : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
requireActivity().bindFingerprintEnrollEnrollingIconTouchDialog()
|
||||
}
|
||||
|
||||
fun Context.bindFingerprintEnrollEnrollingIconTouchDialog(): AlertDialog =
|
||||
AlertDialog.Builder(this, R.style.Theme_AlertDialog)
|
||||
.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title)
|
||||
.setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message)
|
||||
.setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok) {
|
||||
dialog: DialogInterface?, _: Int -> dialog?.dismiss()
|
||||
}
|
||||
.create()
|
@@ -1,513 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.drawable.Animatable2
|
||||
import android.graphics.drawable.AnimatedVectorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AnimationUtils.loadInterpolator
|
||||
import android.view.animation.Interpolator
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
import com.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for rfps
|
||||
*/
|
||||
class FingerprintEnrollEnrollingRfpsFragment : Fragment() {
|
||||
|
||||
private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
|
||||
private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
|
||||
get() = _enrollingViewModel!!
|
||||
|
||||
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||
get() = _progressViewModel!!
|
||||
|
||||
private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
|
||||
private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
|
||||
get() = _errorDialogViewModel!!
|
||||
|
||||
private var fastOutSlowInInterpolator: Interpolator? = null
|
||||
private var linearOutSlowInInterpolator: Interpolator? = null
|
||||
private var fastOutLinearInInterpolator: Interpolator? = null
|
||||
|
||||
private var isAnimationCancelled = false
|
||||
|
||||
private var enrollingView: GlifLayout? = null
|
||||
private val progressBar: ProgressBar
|
||||
get() = enrollingView!!.findViewById(R.id.fingerprint_progress_bar)!!
|
||||
|
||||
private var progressAnim: ObjectAnimator? = null
|
||||
|
||||
private val errorText: TextView
|
||||
get() = enrollingView!!.findViewById(R.id.error_text)!!
|
||||
|
||||
private val iconAnimationDrawable: AnimatedVectorDrawable?
|
||||
get() = (progressBar.background as LayerDrawable)
|
||||
.findDrawableByLayerId(R.id.fingerprint_animation) as AnimatedVectorDrawable?
|
||||
|
||||
private val iconBackgroundBlinksDrawable: AnimatedVectorDrawable?
|
||||
get() = (progressBar.background as LayerDrawable)
|
||||
.findDrawableByLayerId(R.id.fingerprint_background) as AnimatedVectorDrawable?
|
||||
|
||||
private var iconTouchCount = 0
|
||||
|
||||
private val touchAgainRunnable = Runnable {
|
||||
showError(
|
||||
// Use enrollingView to getString to prevent activity is missing during rotation
|
||||
enrollingView!!.context.getString(
|
||||
R.string.security_settings_fingerprint_enroll_lift_touch_again
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val onSkipClickListener = View.OnClickListener { _: View? ->
|
||||
enrollingViewModel.setOnSkipPressed()
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
|
||||
private var enrollingCancelSignal: Any? = null
|
||||
|
||||
private val progressObserver = Observer { progress: EnrollmentProgress? ->
|
||||
if (progress != null && progress.steps >= 0) {
|
||||
onEnrollmentProgressChange(progress)
|
||||
}
|
||||
}
|
||||
|
||||
private val helpMessageObserver = Observer { helpMessage: EnrollmentStatusMessage? ->
|
||||
helpMessage?.let { onEnrollmentHelp(it) }
|
||||
}
|
||||
|
||||
private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
|
||||
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||
errorMessage?.let { onEnrollmentError(it) }
|
||||
}
|
||||
|
||||
private val canceledSignalObserver = Observer { canceledSignal: Any? ->
|
||||
canceledSignal?.let { onEnrollmentCanceled(it) }
|
||||
}
|
||||
|
||||
private val onBackPressedCallback: OnBackPressedCallback =
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
isEnabled = false
|
||||
enrollingViewModel.setOnBackPressed()
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
ViewModelProvider(requireActivity()).let { provider ->
|
||||
_enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
|
||||
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||
_errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
super.onAttach(context)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
enrollingView = inflater.inflate(
|
||||
R.layout.fingerprint_enroll_enrolling, container, false
|
||||
) as GlifLayout
|
||||
return enrollingView!!
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
iconAnimationDrawable!!.registerAnimationCallback(iconAnimationCallback)
|
||||
|
||||
progressBar.setOnTouchListener { _: View?, event: MotionEvent ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
iconTouchCount++
|
||||
if (iconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
|
||||
showIconTouchDialog()
|
||||
} else {
|
||||
progressBar.postDelayed(
|
||||
showDialogRunnable,
|
||||
ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN
|
||||
)
|
||||
}
|
||||
} else if (event.actionMasked == MotionEvent.ACTION_CANCEL
|
||||
|| event.actionMasked == MotionEvent.ACTION_UP
|
||||
) {
|
||||
progressBar.removeCallbacks(showDialogRunnable)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
requireActivity().bindFingerprintEnrollEnrollingRfpsView(
|
||||
view = enrollingView!!,
|
||||
onSkipClickListener = onSkipClickListener
|
||||
)
|
||||
|
||||
fastOutSlowInInterpolator =
|
||||
loadInterpolator(requireContext(), android.R.interpolator.fast_out_slow_in)
|
||||
linearOutSlowInInterpolator =
|
||||
loadInterpolator(requireContext(), android.R.interpolator.linear_out_slow_in)
|
||||
fastOutLinearInInterpolator =
|
||||
loadInterpolator(requireContext(), android.R.interpolator.fast_out_linear_in)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.triggerRetryFlow.collect { retryEnrollment() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryEnrollment() {
|
||||
isAnimationCancelled = false
|
||||
startIconAnimation()
|
||||
startEnrollment()
|
||||
|
||||
clearError()
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isErrorDialogShown = errorDialogViewModel.isDialogShown
|
||||
Log.d(TAG, "onStart(), isEnrolling:$isEnrolling, isErrorDialog:$isErrorDialogShown")
|
||||
if (!isErrorDialogShown) {
|
||||
isAnimationCancelled = false
|
||||
startIconAnimation()
|
||||
startEnrollment()
|
||||
}
|
||||
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
|
||||
private fun startIconAnimation() {
|
||||
iconAnimationDrawable?.start()
|
||||
}
|
||||
|
||||
private fun stopIconAnimation() {
|
||||
isAnimationCancelled = true
|
||||
iconAnimationDrawable?.stop()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
stopIconAnimation()
|
||||
removeEnrollmentObservers()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isConfigChange = requireActivity().isChangingConfigurations
|
||||
Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
|
||||
if (isEnrolling && !isConfigChange) {
|
||||
cancelEnrollment(false)
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun removeEnrollmentObservers() {
|
||||
progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
|
||||
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||
progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
|
||||
}
|
||||
|
||||
private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
|
||||
return
|
||||
}
|
||||
removeEnrollmentObservers()
|
||||
if (waitForLastCancelErrMsg) {
|
||||
progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
|
||||
} else {
|
||||
enrollingCancelSignal = null
|
||||
}
|
||||
val cancelResult: Boolean = progressViewModel.cancelEnrollment()
|
||||
if (!cancelResult) {
|
||||
Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startEnrollment() {
|
||||
enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_ENROLL)
|
||||
if (enrollingCancelSignal == null) {
|
||||
Log.e(TAG, "startEnrollment(), failed")
|
||||
} else {
|
||||
Log.d(TAG, "startEnrollment(), success")
|
||||
}
|
||||
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||
progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
|
||||
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
|
||||
Log.d(TAG, "onEnrollmentHelp($helpMessage)")
|
||||
val helpStr: CharSequence = helpMessage.str
|
||||
if (!TextUtils.isEmpty(helpStr)) {
|
||||
errorText.removeCallbacks(touchAgainRunnable)
|
||||
showError(helpStr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
stopIconAnimation()
|
||||
|
||||
cancelEnrollment(true)
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "newDialog $errorMessage")
|
||||
errorDialogViewModel.newDialog(errorMessage.msgId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentCanceled(canceledSignal: Any) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
|
||||
)
|
||||
if (enrollingCancelSignal === canceledSignal) {
|
||||
progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
|
||||
progressViewModel.clearProgressLiveData()
|
||||
if (enrollingViewModel.onBackPressed) {
|
||||
enrollingViewModel.onCancelledDueToOnBackPressed()
|
||||
} else if (enrollingViewModel.onSkipPressed) {
|
||||
enrollingViewModel.onCancelledDueToOnSkipPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
|
||||
updateProgress(true /* animate */, progress)
|
||||
updateTitleAndDescription()
|
||||
animateFlash()
|
||||
errorText.removeCallbacks(touchAgainRunnable)
|
||||
errorText.postDelayed(touchAgainRunnable, HINT_TIMEOUT_DURATION.toLong())
|
||||
}
|
||||
|
||||
private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
|
||||
val progress = getProgress(enrollmentProgress)
|
||||
Log.d(TAG, "updateProgress($animate, $enrollmentProgress), old:${progressBar.progress}"
|
||||
+ ", new:$progress")
|
||||
|
||||
// Only clear the error when progress has been made.
|
||||
// TODO (b/234772728) Add tests.
|
||||
if (progressBar.progress < progress) {
|
||||
clearError()
|
||||
}
|
||||
if (animate) {
|
||||
animateProgress(progress)
|
||||
} else {
|
||||
progressBar.progress = progress
|
||||
if (progress >= PROGRESS_BAR_MAX) {
|
||||
delayedFinishRunnable.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgress(progress: EnrollmentProgress): Int {
|
||||
if (progress.steps == -1) {
|
||||
return 0
|
||||
}
|
||||
val displayProgress = 0.coerceAtLeast(progress.steps + 1 - progress.remaining)
|
||||
return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
|
||||
}
|
||||
|
||||
private fun showError(error: CharSequence) {
|
||||
errorText.text = error
|
||||
if (errorText.visibility == View.INVISIBLE) {
|
||||
errorText.visibility = View.VISIBLE
|
||||
errorText.translationY = enrollingView!!.context.resources.getDimensionPixelSize(
|
||||
R.dimen.fingerprint_error_text_appear_distance
|
||||
).toFloat()
|
||||
errorText.alpha = 0f
|
||||
errorText.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(linearOutSlowInInterpolator)
|
||||
.start()
|
||||
} else {
|
||||
errorText.animate().cancel()
|
||||
errorText.alpha = 1f
|
||||
errorText.translationY = 0f
|
||||
}
|
||||
if (isResumed && enrollingViewModel.isAccessibilityEnabled) {
|
||||
enrollingViewModel.vibrateError(javaClass.simpleName + "::showError")
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearError() {
|
||||
if (errorText.visibility == View.VISIBLE) {
|
||||
errorText.animate()
|
||||
.alpha(0f)
|
||||
.translationY(
|
||||
resources.getDimensionPixelSize(
|
||||
R.dimen.fingerprint_error_text_disappear_distance
|
||||
).toFloat()
|
||||
)
|
||||
.setDuration(100)
|
||||
.setInterpolator(fastOutLinearInInterpolator)
|
||||
.withEndAction { errorText.visibility = View.INVISIBLE }
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateProgress(progress: Int) {
|
||||
progressAnim?.cancel()
|
||||
val anim = ObjectAnimator.ofInt(
|
||||
progressBar /* target */,
|
||||
"progress" /* propertyName */,
|
||||
progressBar.progress /* values[0] */,
|
||||
progress /* values[1] */
|
||||
)
|
||||
anim.addListener(progressAnimationListener)
|
||||
anim.interpolator = fastOutSlowInInterpolator
|
||||
anim.setDuration(ANIMATION_DURATION)
|
||||
anim.start()
|
||||
progressAnim = anim
|
||||
}
|
||||
|
||||
private fun animateFlash() {
|
||||
iconBackgroundBlinksDrawable?.start()
|
||||
}
|
||||
|
||||
private fun updateTitleAndDescription() {
|
||||
val progressLiveData: EnrollmentProgress = progressViewModel.progressLiveData.value!!
|
||||
GlifLayoutHelper(activity!!, enrollingView!!).setDescriptionText(
|
||||
enrollingView!!.context.getString(
|
||||
if (progressLiveData.steps == -1)
|
||||
R.string.security_settings_fingerprint_enroll_start_message
|
||||
else
|
||||
R.string.security_settings_fingerprint_enroll_repeat_message
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun showIconTouchDialog() {
|
||||
iconTouchCount = 0
|
||||
enrollingViewModel.showIconTouchDialog()
|
||||
}
|
||||
|
||||
private val showDialogRunnable = Runnable { showIconTouchDialog() }
|
||||
|
||||
private val progressAnimationListener: Animator.AnimatorListener =
|
||||
object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
startIconAnimation()
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator) {}
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
stopIconAnimation()
|
||||
if (progressBar.progress >= PROGRESS_BAR_MAX) {
|
||||
progressBar.postDelayed(delayedFinishRunnable, ANIMATION_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {}
|
||||
}
|
||||
|
||||
// Give the user a chance to see progress completed before jumping to the next stage.
|
||||
private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
|
||||
|
||||
private val iconAnimationCallback: Animatable2.AnimationCallback =
|
||||
object : Animatable2.AnimationCallback() {
|
||||
override fun onAnimationEnd(d: Drawable) {
|
||||
if (isAnimationCancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
// Start animation after it has ended.
|
||||
progressBar.post { startIconAnimation() }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "FingerprintEnrollEnrollingRfpsFragment"
|
||||
private const val PROGRESS_BAR_MAX = 10000
|
||||
private const val ANIMATION_DURATION = 250L
|
||||
private const val ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN: Long = 500
|
||||
private const val ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3
|
||||
|
||||
/**
|
||||
* If we don't see progress during this time, we show an error message to remind the users that
|
||||
* they need to lift the finger and touch again.
|
||||
*/
|
||||
private const val HINT_TIMEOUT_DURATION = 2500
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollEnrollingRfpsView(
|
||||
view: GlifLayout,
|
||||
onSkipClickListener: View.OnClickListener
|
||||
) {
|
||||
GlifLayoutHelper(this, view).let {
|
||||
it.setDescriptionText(
|
||||
getString(
|
||||
R.string.security_settings_fingerprint_enroll_start_message
|
||||
)
|
||||
)
|
||||
it.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||
}
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||
.progressBackgroundTintMode = PorterDuff.Mode.SRC
|
||||
|
||||
view.getMixin(FooterBarMixin::class.java).secondaryButton =
|
||||
FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setListener(onSkipClickListener)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
}
|
@@ -1,669 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.RawRes
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.animation.Interpolator
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.airbnb.lottie.LottieComposition
|
||||
import com.airbnb.lottie.LottieCompositionFactory
|
||||
import com.airbnb.lottie.LottieProperty
|
||||
import com.airbnb.lottie.model.KeyPath
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
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.DescriptionMixin
|
||||
import com.google.android.setupdesign.template.HeaderMixin
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for sfps
|
||||
*/
|
||||
class FingerprintEnrollEnrollingSfpsFragment : Fragment() {
|
||||
|
||||
private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
|
||||
private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
|
||||
get() = _enrollingViewModel!!
|
||||
|
||||
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||
get() = _progressViewModel!!
|
||||
|
||||
private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
|
||||
private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
|
||||
get() = _errorDialogViewModel!!
|
||||
|
||||
private val fastOutSlowInInterpolator: Interpolator
|
||||
get() = AnimationUtils.loadInterpolator(
|
||||
activity,
|
||||
androidx.appcompat.R.interpolator.fast_out_slow_in,
|
||||
)
|
||||
|
||||
private var enrollingView: GlifLayout? = null
|
||||
|
||||
private val progressBar: ProgressBar
|
||||
get() = enrollingView!!.findViewById(R.id.fingerprint_progress_bar)!!
|
||||
|
||||
private var progressAnim: ObjectAnimator? = null
|
||||
|
||||
private val progressAnimationListener: Animator.AnimatorListener =
|
||||
object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {}
|
||||
override fun onAnimationRepeat(animation: Animator) {}
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
if (progressBar.progress >= PROGRESS_BAR_MAX) {
|
||||
progressBar.postDelayed(delayedFinishRunnable, PROGRESS_ANIMATION_DURATION)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {}
|
||||
}
|
||||
|
||||
private val illustrationLottie: LottieAnimationView
|
||||
get() = enrollingView!!.findViewById(R.id.illustration_lottie)!!
|
||||
|
||||
private var haveShownSfpsNoAnimationLottie = false
|
||||
private var haveShownSfpsCenterLottie = false
|
||||
private var haveShownSfpsTipLottie = false
|
||||
private var haveShownSfpsLeftEdgeLottie = false
|
||||
private var haveShownSfpsRightEdgeLottie = false
|
||||
|
||||
private var helpAnimation: ObjectAnimator? = null
|
||||
|
||||
private var iconTouchCount = 0
|
||||
|
||||
private val showIconTouchDialogRunnable = Runnable { showIconTouchDialog() }
|
||||
|
||||
private var enrollingCancelSignal: Any? = null
|
||||
|
||||
// Give the user a chance to see progress completed before jumping to the next stage.
|
||||
private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
|
||||
|
||||
private val onSkipClickListener = View.OnClickListener { _: View? ->
|
||||
enrollingViewModel.setOnSkipPressed()
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
|
||||
private val progressObserver = Observer { progress: EnrollmentProgress? ->
|
||||
if (progress != null && progress.steps >= 0) {
|
||||
onEnrollmentProgressChange(progress)
|
||||
}
|
||||
}
|
||||
|
||||
private val helpMessageObserver = Observer { helpMessage: EnrollmentStatusMessage? ->
|
||||
helpMessage?.let { onEnrollmentHelp(it) }
|
||||
}
|
||||
|
||||
private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
|
||||
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||
errorMessage?.let { onEnrollmentError(it) }
|
||||
}
|
||||
|
||||
private val canceledSignalObserver = Observer { canceledSignal: Any? ->
|
||||
Log.d(TAG, "canceledSignalObserver($canceledSignal)")
|
||||
canceledSignal?.let { onEnrollmentCanceled(it) }
|
||||
}
|
||||
|
||||
private val onBackPressedCallback: OnBackPressedCallback =
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
isEnabled = false
|
||||
enrollingViewModel.setOnBackPressed()
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
ViewModelProvider(requireActivity()).let { provider ->
|
||||
_enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
|
||||
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||
_errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
super.onAttach(context)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
enrollingView = inflater.inflate(
|
||||
R.layout.sfps_enroll_enrolling,
|
||||
container, false
|
||||
) as GlifLayout
|
||||
return enrollingView
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
requireActivity().bindFingerprintEnrollEnrollingSfpsView(
|
||||
view = enrollingView!!,
|
||||
onSkipClickListener = onSkipClickListener
|
||||
)
|
||||
|
||||
// setHelpAnimation()
|
||||
helpAnimation = ObjectAnimator.ofFloat(
|
||||
enrollingView!!.findViewById<RelativeLayout>(R.id.progress_lottie)!!,
|
||||
"translationX" /* propertyName */,
|
||||
0f,
|
||||
HELP_ANIMATION_TRANSLATION_X,
|
||||
-1 * HELP_ANIMATION_TRANSLATION_X,
|
||||
HELP_ANIMATION_TRANSLATION_X,
|
||||
0f
|
||||
).also {
|
||||
it.interpolator = AccelerateDecelerateInterpolator()
|
||||
it.setDuration(HELP_ANIMATION_DURATION)
|
||||
it.setAutoCancel(false)
|
||||
}
|
||||
|
||||
progressBar.setOnTouchListener { _: View?, event: MotionEvent ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
iconTouchCount++
|
||||
if (iconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
|
||||
showIconTouchDialog()
|
||||
} else {
|
||||
progressBar.postDelayed(
|
||||
showIconTouchDialogRunnable,
|
||||
ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN
|
||||
)
|
||||
}
|
||||
} else if (event.actionMasked == MotionEvent.ACTION_CANCEL
|
||||
|| event.actionMasked == MotionEvent.ACTION_UP
|
||||
) {
|
||||
progressBar.removeCallbacks(showIconTouchDialogRunnable)
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.triggerRetryFlow.collect { retryEnrollment() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryEnrollment() {
|
||||
startEnrollment()
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isErrorDialogShown = errorDialogViewModel.isDialogShown
|
||||
Log.d(TAG, "onStart(), isEnrolling:$isEnrolling, isErrorDialog:$isErrorDialogShown")
|
||||
if (!isErrorDialogShown) {
|
||||
startEnrollment()
|
||||
}
|
||||
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
progressViewModel.helpMessageLiveData.value.let {
|
||||
if (it != null) {
|
||||
onEnrollmentHelp(it)
|
||||
} else {
|
||||
clearError()
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
removeEnrollmentObservers()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isConfigChange = requireActivity().isChangingConfigurations
|
||||
Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
|
||||
if (isEnrolling && !isConfigChange) {
|
||||
cancelEnrollment(false)
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun removeEnrollmentObservers() {
|
||||
progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
|
||||
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||
progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
|
||||
}
|
||||
|
||||
private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
|
||||
return
|
||||
}
|
||||
removeEnrollmentObservers()
|
||||
if (waitForLastCancelErrMsg) {
|
||||
progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
|
||||
} else {
|
||||
enrollingCancelSignal = null
|
||||
}
|
||||
val cancelResult: Boolean = progressViewModel.cancelEnrollment()
|
||||
if (!cancelResult) {
|
||||
Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startEnrollment() {
|
||||
enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_ENROLL)
|
||||
if (enrollingCancelSignal == null) {
|
||||
Log.e(TAG, "startEnrollment(), failed")
|
||||
} else {
|
||||
Log.d(TAG, "startEnrollment(), success")
|
||||
}
|
||||
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||
progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
|
||||
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun configureEnrollmentStage(description: CharSequence, @RawRes lottie: Int) {
|
||||
GlifLayoutHelper(requireActivity(), enrollingView!!).setDescriptionText(description)
|
||||
LottieCompositionFactory.fromRawRes(activity, lottie)
|
||||
.addListener { c: LottieComposition ->
|
||||
illustrationLottie.setComposition(c)
|
||||
illustrationLottie.visibility = View.VISIBLE
|
||||
illustrationLottie.playAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private val currentSfpsStage: Int
|
||||
get() {
|
||||
val progressLiveData: EnrollmentProgress =
|
||||
progressViewModel.progressLiveData.value
|
||||
?: return STAGE_UNKNOWN
|
||||
val progressSteps: Int = progressLiveData.steps - progressLiveData.remaining
|
||||
return if (progressSteps < getStageThresholdSteps(0)) {
|
||||
SFPS_STAGE_NO_ANIMATION
|
||||
} else if (progressSteps < getStageThresholdSteps(1)) {
|
||||
SFPS_STAGE_CENTER
|
||||
} else if (progressSteps < getStageThresholdSteps(2)) {
|
||||
SFPS_STAGE_FINGERTIP
|
||||
} else if (progressSteps < getStageThresholdSteps(3)) {
|
||||
SFPS_STAGE_LEFT_EDGE
|
||||
} else {
|
||||
SFPS_STAGE_RIGHT_EDGE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
|
||||
Log.d(TAG, "onEnrollmentHelp($helpMessage)")
|
||||
val helpStr: CharSequence = helpMessage.str
|
||||
if (helpStr.isNotEmpty()) {
|
||||
showError(helpStr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
cancelEnrollment(true)
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "newDialog $errorMessage")
|
||||
errorDialogViewModel.newDialog(errorMessage.msgId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentCanceled(canceledSignal: Any) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
|
||||
)
|
||||
if (enrollingCancelSignal === canceledSignal) {
|
||||
progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
|
||||
progressViewModel.clearProgressLiveData()
|
||||
if (enrollingViewModel.onBackPressed) {
|
||||
enrollingViewModel.onCancelledDueToOnBackPressed()
|
||||
} else if (enrollingViewModel.onSkipPressed) {
|
||||
enrollingViewModel.onCancelledDueToOnSkipPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun announceEnrollmentProgress(announcement: CharSequence) {
|
||||
enrollingViewModel.sendAccessibilityEvent(announcement)
|
||||
}
|
||||
|
||||
private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
|
||||
updateProgress(true /* animate */, progress)
|
||||
if (enrollingViewModel.isAccessibilityEnabled) {
|
||||
val percent: Int =
|
||||
((progress.steps - progress.remaining).toFloat() / progress.steps.toFloat() * 100).toInt()
|
||||
val announcement: CharSequence = getString(
|
||||
R.string.security_settings_sfps_enroll_progress_a11y_message, percent
|
||||
)
|
||||
announceEnrollmentProgress(announcement)
|
||||
illustrationLottie.contentDescription =
|
||||
getString(R.string.security_settings_sfps_animation_a11y_label, percent)
|
||||
}
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
|
||||
private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "Enrollment not started yet")
|
||||
return
|
||||
}
|
||||
|
||||
val progress = getProgress(enrollmentProgress)
|
||||
Log.d(TAG, "updateProgress($animate, $enrollmentProgress), old:${progressBar.progress}"
|
||||
+ ", new:$progress")
|
||||
|
||||
// Only clear the error when progress has been made.
|
||||
// TODO (b/234772728) Add tests.
|
||||
if (progressBar.progress < progress) {
|
||||
clearError()
|
||||
}
|
||||
if (animate) {
|
||||
animateProgress(progress)
|
||||
} else {
|
||||
progressBar.progress = progress
|
||||
if (progress >= PROGRESS_BAR_MAX) {
|
||||
delayedFinishRunnable.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgress(progress: EnrollmentProgress): Int {
|
||||
if (progress.steps == -1) {
|
||||
return 0
|
||||
}
|
||||
val displayProgress = 0.coerceAtLeast(progress.steps + 1 - progress.remaining)
|
||||
return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
|
||||
}
|
||||
|
||||
private fun showError(error: CharSequence) {
|
||||
enrollingView!!.let {
|
||||
it.headerText = error
|
||||
it.headerTextView.contentDescription = error
|
||||
GlifLayoutHelper(requireActivity(), it).setDescriptionText("")
|
||||
}
|
||||
|
||||
if (isResumed && !helpAnimation!!.isRunning) {
|
||||
helpAnimation!!.start()
|
||||
}
|
||||
applySfpsErrorDynamicColors(true)
|
||||
if (isResumed && enrollingViewModel.isAccessibilityEnabled) {
|
||||
enrollingViewModel.vibrateError(javaClass.simpleName + "::showError")
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearError() {
|
||||
applySfpsErrorDynamicColors(false)
|
||||
}
|
||||
|
||||
private fun animateProgress(progress: Int) {
|
||||
progressAnim?.cancel()
|
||||
progressAnim = ObjectAnimator.ofInt(
|
||||
progressBar,
|
||||
"progress",
|
||||
progressBar.progress,
|
||||
progress
|
||||
).also {
|
||||
it.addListener(progressAnimationListener)
|
||||
it.interpolator = fastOutSlowInInterpolator
|
||||
it.setDuration(PROGRESS_ANIMATION_DURATION)
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies dynamic colors corresponding to showing or clearing errors on the progress bar
|
||||
* and finger lottie for SFPS
|
||||
*/
|
||||
private fun applySfpsErrorDynamicColors(isError: Boolean) {
|
||||
progressBar.applyProgressBarDynamicColor(requireContext(), isError)
|
||||
illustrationLottie.applyLottieDynamicColor(requireContext(), isError)
|
||||
}
|
||||
|
||||
private fun getStageThresholdSteps(index: Int): Int {
|
||||
val progressLiveData: EnrollmentProgress? =
|
||||
progressViewModel.progressLiveData.value
|
||||
if (progressLiveData == null || progressLiveData.steps == -1) {
|
||||
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet")
|
||||
return 1
|
||||
}
|
||||
return (progressLiveData.steps
|
||||
* enrollingViewModel.getEnrollStageThreshold(index)).roundToInt()
|
||||
}
|
||||
|
||||
private fun updateTitleAndDescription() {
|
||||
val helper = GlifLayoutHelper(requireActivity(), enrollingView!!)
|
||||
if (enrollingViewModel.isAccessibilityEnabled) {
|
||||
enrollingViewModel.clearTalkback()
|
||||
helper.glifLayout.descriptionTextView.accessibilityLiveRegion =
|
||||
View.ACCESSIBILITY_LIVE_REGION_POLITE
|
||||
}
|
||||
val stage = currentSfpsStage
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG, "updateTitleAndDescription, stage:" + stage
|
||||
+ ", noAnimation:" + haveShownSfpsNoAnimationLottie
|
||||
+ ", center:" + haveShownSfpsCenterLottie
|
||||
+ ", tip:" + haveShownSfpsTipLottie
|
||||
+ ", leftEdge:" + haveShownSfpsLeftEdgeLottie
|
||||
+ ", rightEdge:" + haveShownSfpsRightEdgeLottie
|
||||
)
|
||||
}
|
||||
when (stage) {
|
||||
SFPS_STAGE_NO_ANIMATION -> {
|
||||
helper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||
if (!haveShownSfpsNoAnimationLottie) {
|
||||
haveShownSfpsNoAnimationLottie = true
|
||||
illustrationLottie.contentDescription =
|
||||
getString(R.string.security_settings_sfps_animation_a11y_label, 0)
|
||||
configureEnrollmentStage(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||
R.raw.sfps_lottie_no_animation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_CENTER -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_finger_center_title)
|
||||
if (!haveShownSfpsCenterLottie) {
|
||||
haveShownSfpsCenterLottie = true
|
||||
configureEnrollmentStage(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message),
|
||||
R.raw.sfps_lottie_pad_center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_FINGERTIP -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_fingertip_title)
|
||||
if (!haveShownSfpsTipLottie) {
|
||||
haveShownSfpsTipLottie = true
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_tip)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_LEFT_EDGE -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_left_edge_title)
|
||||
if (!haveShownSfpsLeftEdgeLottie) {
|
||||
haveShownSfpsLeftEdgeLottie = true
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_left_edge)
|
||||
}
|
||||
}
|
||||
|
||||
SFPS_STAGE_RIGHT_EDGE -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_right_edge_title)
|
||||
if (!haveShownSfpsRightEdgeLottie) {
|
||||
haveShownSfpsRightEdgeLottie = true
|
||||
configureEnrollmentStage("", R.raw.sfps_lottie_right_edge)
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_UNKNOWN -> {
|
||||
// Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
|
||||
// which gets announced for a11y upon entering the page. For SFPS, we want to
|
||||
// announce a different string for a11y upon entering the page.
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||
helper.setDescriptionText(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message)
|
||||
)
|
||||
val description: CharSequence = getString(
|
||||
R.string.security_settings_sfps_enroll_find_sensor_message
|
||||
)
|
||||
helper.glifLayout.headerTextView.contentDescription = description
|
||||
helper.activity.title = description
|
||||
}
|
||||
|
||||
else -> {
|
||||
helper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||
helper.setDescriptionText(
|
||||
getString(R.string.security_settings_sfps_enroll_start_message)
|
||||
)
|
||||
val description: CharSequence = getString(
|
||||
R.string.security_settings_sfps_enroll_find_sensor_message
|
||||
)
|
||||
helper.glifLayout.headerTextView.contentDescription = description
|
||||
helper.activity.title = description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showIconTouchDialog() {
|
||||
iconTouchCount = 0
|
||||
enrollingViewModel.showIconTouchDialog()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = FingerprintEnrollEnrollingSfpsFragment::class.java.simpleName
|
||||
private const val DEBUG = false
|
||||
private const val PROGRESS_BAR_MAX = 10000
|
||||
private const val HELP_ANIMATION_DURATION = 550L
|
||||
private const val HELP_ANIMATION_TRANSLATION_X = 40f
|
||||
private const val PROGRESS_ANIMATION_DURATION = 250L
|
||||
private const val ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN: Long = 500
|
||||
private const val ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3
|
||||
private const val STAGE_UNKNOWN = -1
|
||||
private const val SFPS_STAGE_NO_ANIMATION = 0
|
||||
private const val SFPS_STAGE_CENTER = 1
|
||||
private const val SFPS_STAGE_FINGERTIP = 2
|
||||
private const val SFPS_STAGE_LEFT_EDGE = 3
|
||||
private const val SFPS_STAGE_RIGHT_EDGE = 4
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollEnrollingSfpsView(
|
||||
view: GlifLayout,
|
||||
onSkipClickListener: View.OnClickListener
|
||||
) {
|
||||
GlifLayoutHelper(this, view).setDescriptionText(
|
||||
getString(R.string.security_settings_fingerprint_enroll_start_message)
|
||||
)
|
||||
|
||||
view.getMixin(FooterBarMixin::class.java).secondaryButton = FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setListener(onSkipClickListener)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!.progressBackgroundTintMode =
|
||||
PorterDuff.Mode.SRC
|
||||
|
||||
view.findViewById<ProgressBar>(R.id.fingerprint_progress_bar)!!
|
||||
.applyProgressBarDynamicColor(this, false)
|
||||
|
||||
view.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!
|
||||
.applyLottieDynamicColor(this, false)
|
||||
|
||||
view.maybeHideSfpsText(resources.configuration.orientation)
|
||||
}
|
||||
|
||||
private fun ProgressBar.applyProgressBarDynamicColor(context: Context, isError: Boolean) {
|
||||
progressTintList = ColorStateList.valueOf(
|
||||
context.getColor(
|
||||
if (isError)
|
||||
R.color.sfps_enrollment_progress_bar_error_color
|
||||
else
|
||||
R.color.sfps_enrollment_progress_bar_fill_color
|
||||
)
|
||||
)
|
||||
progressTintMode = PorterDuff.Mode.SRC
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun LottieAnimationView.applyLottieDynamicColor(context: Context, isError: Boolean) {
|
||||
addValueCallback(
|
||||
KeyPath(".blue100", "**"),
|
||||
LottieProperty.COLOR_FILTER
|
||||
) {
|
||||
PorterDuffColorFilter(
|
||||
context.getColor(
|
||||
if (isError)
|
||||
R.color.sfps_enrollment_fp_error_color
|
||||
else
|
||||
R.color.sfps_enrollment_fp_captured_color
|
||||
),
|
||||
PorterDuff.Mode.SRC_ATOP
|
||||
)
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun GlifLayout.maybeHideSfpsText(@Configuration.Orientation orientation: Int) {
|
||||
val headerMixin: HeaderMixin = getMixin(HeaderMixin::class.java)
|
||||
val descriptionMixin: DescriptionMixin = getMixin(DescriptionMixin::class.java)
|
||||
|
||||
val isLandscape = (orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
headerMixin.setAutoTextSizeEnabled(isLandscape)
|
||||
if (isLandscape) {
|
||||
headerMixin.textView.minLines = 0
|
||||
headerMixin.textView.maxLines = 10
|
||||
descriptionMixin.textView.minLines = 0
|
||||
descriptionMixin.textView.maxLines = 10
|
||||
} else {
|
||||
headerMixin.textView.setLines(4)
|
||||
// hide the description
|
||||
descriptionMixin.textView.setLines(0)
|
||||
}
|
||||
}
|
@@ -1,707 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.annotation.RawRes
|
||||
import android.content.Context
|
||||
import android.hardware.biometrics.BiometricFingerprintConstants
|
||||
import android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL
|
||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Surface
|
||||
import android.view.Surface.ROTATION_270
|
||||
import android.view.Surface.ROTATION_90
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.airbnb.lottie.LottieComposition
|
||||
import com.airbnb.lottie.LottieCompositionFactory
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
import com.android.settings.biometrics2.ui.widget.UdfpsEnrollView
|
||||
import com.android.settingslib.display.DisplayDensityUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Fragment is used to handle enrolling process for udfps
|
||||
*/
|
||||
class FingerprintEnrollEnrollingUdfpsFragment : Fragment() {
|
||||
|
||||
private var _enrollingViewModel: FingerprintEnrollEnrollingViewModel? = null
|
||||
private val enrollingViewModel: FingerprintEnrollEnrollingViewModel
|
||||
get() = _enrollingViewModel!!
|
||||
|
||||
private var _rotationViewModel: DeviceRotationViewModel? = null
|
||||
private val rotationViewModel: DeviceRotationViewModel
|
||||
get() = _rotationViewModel!!
|
||||
|
||||
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||
get() = _progressViewModel!!
|
||||
|
||||
private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
|
||||
private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
|
||||
get() = _errorDialogViewModel!!
|
||||
|
||||
private var illustrationLottie: LottieAnimationView? = null
|
||||
|
||||
private var haveShownTipLottie = false
|
||||
private var haveShownLeftEdgeLottie = false
|
||||
private var haveShownRightEdgeLottie = false
|
||||
private var haveShownCenterLottie = false
|
||||
private var haveShownGuideLottie = false
|
||||
|
||||
private var enrollingView: RelativeLayout? = null
|
||||
|
||||
private val titleText: TextView
|
||||
get() = enrollingView!!.findViewById(R.id.suc_layout_title)!!
|
||||
|
||||
private val subTitleText: TextView
|
||||
get() = enrollingView!!.findViewById(R.id.sud_layout_subtitle)!!
|
||||
|
||||
private val udfpsEnrollView: UdfpsEnrollView
|
||||
get() = enrollingView!!.findViewById(R.id.udfps_animation_view)!!
|
||||
|
||||
private val skipBtn: Button
|
||||
get() = enrollingView!!.findViewById(R.id.skip_btn)!!
|
||||
|
||||
private val icon: ImageView
|
||||
get() = enrollingView!!.findViewById(R.id.sud_layout_icon)!!
|
||||
|
||||
private val shouldShowLottie: Boolean
|
||||
get() {
|
||||
val displayDensity = DisplayDensityUtils(requireContext())
|
||||
val currentDensityIndex: Int = displayDensity.currentIndexForDefaultDisplay
|
||||
val currentDensity: Int =
|
||||
displayDensity.defaultDisplayDensityValues[currentDensityIndex]
|
||||
val defaultDensity: Int = displayDensity.defaultDensityForDefaultDisplay
|
||||
return defaultDensity == currentDensity
|
||||
}
|
||||
|
||||
private val isAccessibilityEnabled
|
||||
get() = enrollingViewModel.isAccessibilityEnabled
|
||||
|
||||
private var rotation = -1
|
||||
|
||||
private var enrollingCancelSignal: Any? = null
|
||||
|
||||
private val onSkipClickListener = View.OnClickListener { _: View? ->
|
||||
enrollingViewModel.setOnSkipPressed()
|
||||
cancelEnrollment(true) // TODO Add test after b/273640000 fixed
|
||||
}
|
||||
|
||||
private val progressObserver = Observer { progress: EnrollmentProgress? ->
|
||||
if (progress != null && progress.steps >= 0) {
|
||||
onEnrollmentProgressChange(progress)
|
||||
}
|
||||
}
|
||||
|
||||
private val helpMessageObserver = Observer { helpMessage: EnrollmentStatusMessage? ->
|
||||
Log.d(TAG, "helpMessageObserver($helpMessage)")
|
||||
helpMessage?.let { onEnrollmentHelp(it) }
|
||||
}
|
||||
|
||||
private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
|
||||
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||
errorMessage?.let { onEnrollmentError(it) }
|
||||
}
|
||||
|
||||
private val canceledSignalObserver = Observer { canceledSignal: Any? ->
|
||||
Log.d(TAG, "canceledSignalObserver($canceledSignal)")
|
||||
canceledSignal?.let { onEnrollmentCanceled(it) }
|
||||
}
|
||||
|
||||
private val acquireObserver =
|
||||
Observer { isAcquiredGood: Boolean? -> isAcquiredGood?.let { onAcquired(it) } }
|
||||
|
||||
private val pointerDownObserver =
|
||||
Observer { sensorId: Int? -> sensorId?.let { onPointerDown(it) } }
|
||||
|
||||
private val pointerUpObserver =
|
||||
Observer { sensorId: Int? -> sensorId?.let { onPointerUp(it) } }
|
||||
|
||||
private val rotationObserver =
|
||||
Observer { rotation: Int? -> rotation?.let { onRotationChanged(it) } }
|
||||
|
||||
private val onBackPressedCallback: OnBackPressedCallback =
|
||||
object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
isEnabled = false
|
||||
enrollingViewModel.setOnBackPressed()
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Give the user a chance to see progress completed before jumping to the next stage.
|
||||
private val delayedFinishRunnable = Runnable { enrollingViewModel.onEnrollingDone() }
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
ViewModelProvider(requireActivity()).let { provider ->
|
||||
_enrollingViewModel = provider[FingerprintEnrollEnrollingViewModel::class.java]
|
||||
_rotationViewModel = provider[DeviceRotationViewModel::class.java]
|
||||
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||
_errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
super.onAttach(context)
|
||||
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = (inflater.inflate(
|
||||
R.layout.udfps_enroll_enrolling_v2, container, false
|
||||
) as RelativeLayout).also {
|
||||
enrollingView = it
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
rotation = rotationViewModel.liveData.value!!
|
||||
updateIllustrationLottie(rotation)
|
||||
|
||||
requireActivity().bindFingerprintEnrollEnrollingUdfpsView(
|
||||
view = enrollingView!!,
|
||||
sensorProperties = enrollingViewModel.firstFingerprintSensorPropertiesInternal!!,
|
||||
rotation = rotation,
|
||||
onSkipClickListener = onSkipClickListener,
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.triggerRetryFlow.collect { retryEnrollment() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryEnrollment() {
|
||||
reattachUdfpsEnrollView()
|
||||
|
||||
startEnrollment()
|
||||
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
progressViewModel.helpMessageLiveData.value.let {
|
||||
if (it != null) {
|
||||
onEnrollmentHelp(it)
|
||||
} else {
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isErrorDialogShown = errorDialogViewModel.isDialogShown
|
||||
Log.d(TAG, "onStart(), isEnrolling:$isEnrolling, isErrorDialog:$isErrorDialogShown")
|
||||
if (!isErrorDialogShown) {
|
||||
startEnrollment()
|
||||
}
|
||||
|
||||
updateProgress(false /* animate */, progressViewModel.progressLiveData.value!!)
|
||||
progressViewModel.helpMessageLiveData.value.let {
|
||||
if (it != null) {
|
||||
onEnrollmentHelp(it)
|
||||
} else {
|
||||
updateTitleAndDescription()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reattachUdfpsEnrollView() {
|
||||
enrollingView!!.let {
|
||||
val newUdfpsView = LayoutInflater.from(requireActivity()).inflate(
|
||||
R.layout.udfps_enroll_enrolling_v2_udfps_view,
|
||||
null
|
||||
)
|
||||
val index = it.indexOfChild(udfpsEnrollView)
|
||||
val lp = udfpsEnrollView.layoutParams
|
||||
|
||||
it.removeView(udfpsEnrollView)
|
||||
it.addView(newUdfpsView, index, lp)
|
||||
udfpsEnrollView.setSensorProperties(
|
||||
enrollingViewModel.firstFingerprintSensorPropertiesInternal
|
||||
)
|
||||
}
|
||||
|
||||
// Clear lottie status
|
||||
haveShownTipLottie = false
|
||||
haveShownLeftEdgeLottie = false
|
||||
haveShownRightEdgeLottie = false
|
||||
haveShownCenterLottie = false
|
||||
haveShownGuideLottie = false
|
||||
illustrationLottie?.let {
|
||||
it.contentDescription = ""
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
rotationViewModel.liveData.observe(this, rotationObserver)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
rotationViewModel.liveData.removeObserver(rotationObserver)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
removeEnrollmentObservers()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isConfigChange = requireActivity().isChangingConfigurations
|
||||
Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
|
||||
if (isEnrolling && !isConfigChange) {
|
||||
cancelEnrollment(false)
|
||||
}
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun removeEnrollmentObservers() {
|
||||
progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
|
||||
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||
progressViewModel.helpMessageLiveData.removeObserver(helpMessageObserver)
|
||||
progressViewModel.acquireLiveData.removeObserver(acquireObserver)
|
||||
progressViewModel.pointerDownLiveData.removeObserver(pointerDownObserver)
|
||||
progressViewModel.pointerUpLiveData.removeObserver(pointerUpObserver)
|
||||
}
|
||||
|
||||
private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
|
||||
return
|
||||
}
|
||||
removeEnrollmentObservers()
|
||||
if (waitForLastCancelErrMsg) {
|
||||
progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
|
||||
} else {
|
||||
enrollingCancelSignal = null
|
||||
}
|
||||
val cancelResult: Boolean = progressViewModel.cancelEnrollment()
|
||||
if (!cancelResult) {
|
||||
Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startEnrollment() {
|
||||
enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_ENROLL)
|
||||
if (enrollingCancelSignal == null) {
|
||||
Log.e(TAG, "startEnrollment(), failed")
|
||||
} else {
|
||||
Log.d(TAG, "startEnrollment(), success")
|
||||
}
|
||||
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||
progressViewModel.helpMessageLiveData.observe(this, helpMessageObserver)
|
||||
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||
progressViewModel.acquireLiveData.observe(this, acquireObserver)
|
||||
progressViewModel.pointerDownLiveData.observe(this, pointerDownObserver)
|
||||
progressViewModel.pointerUpLiveData.observe(this, pointerUpObserver)
|
||||
}
|
||||
|
||||
private fun updateProgress(animate: Boolean, enrollmentProgress: EnrollmentProgress) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "Enrollment not started yet")
|
||||
return
|
||||
}
|
||||
|
||||
val progress = getProgress(enrollmentProgress)
|
||||
Log.d(TAG, "updateProgress($animate, $enrollmentProgress), progress:$progress")
|
||||
|
||||
if (enrollmentProgress.steps != -1) {
|
||||
udfpsEnrollView.onEnrollmentProgress(
|
||||
enrollmentProgress.remaining,
|
||||
enrollmentProgress.steps
|
||||
)
|
||||
}
|
||||
|
||||
if (progress >= PROGRESS_BAR_MAX) {
|
||||
if (animate) {
|
||||
// Wait animations to finish, then proceed to next page
|
||||
activity!!.mainThreadHandler.postDelayed(delayedFinishRunnable, 400L)
|
||||
} else {
|
||||
delayedFinishRunnable.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgress(progress: EnrollmentProgress): Int {
|
||||
if (progress.steps == -1) {
|
||||
return 0
|
||||
}
|
||||
val displayProgress = 0.coerceAtLeast(progress.steps + 1 - progress.remaining)
|
||||
return PROGRESS_BAR_MAX * displayProgress / (progress.steps + 1)
|
||||
}
|
||||
|
||||
private fun updateTitleAndDescription() {
|
||||
Log.d(TAG, "updateTitleAndDescription($currentStage)")
|
||||
when (currentStage) {
|
||||
STAGE_CENTER -> {
|
||||
titleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||
if (isAccessibilityEnabled || illustrationLottie == null) {
|
||||
subTitleText.setText(R.string.security_settings_udfps_enroll_start_message)
|
||||
} else if (!haveShownCenterLottie) {
|
||||
haveShownCenterLottie = true
|
||||
// Note: Update string reference when differentiate in between udfps & sfps
|
||||
illustrationLottie!!.contentDescription = getString(R.string.security_settings_sfps_enroll_finger_center_title)
|
||||
configureEnrollmentStage(R.raw.udfps_center_hint_lottie)
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_GUIDED -> {
|
||||
titleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title)
|
||||
if (isAccessibilityEnabled || illustrationLottie == null) {
|
||||
subTitleText.setText(
|
||||
R.string.security_settings_udfps_enroll_repeat_a11y_message
|
||||
)
|
||||
} else if (!haveShownGuideLottie) {
|
||||
haveShownGuideLottie = true
|
||||
illustrationLottie!!.contentDescription =
|
||||
getString(R.string.security_settings_fingerprint_enroll_repeat_message)
|
||||
// TODO(b/228100413) Could customize guided lottie animation
|
||||
configureEnrollmentStage(R.raw.udfps_center_hint_lottie)
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_FINGERTIP -> {
|
||||
titleText.setText(R.string.security_settings_udfps_enroll_fingertip_title)
|
||||
if (!haveShownTipLottie && illustrationLottie != null) {
|
||||
haveShownTipLottie = true
|
||||
illustrationLottie!!.contentDescription =
|
||||
getString(R.string.security_settings_udfps_tip_fingerprint_help)
|
||||
configureEnrollmentStage(R.raw.udfps_tip_hint_lottie)
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_LEFT_EDGE -> {
|
||||
titleText.setText(R.string.security_settings_udfps_enroll_left_edge_title)
|
||||
if (!haveShownLeftEdgeLottie && illustrationLottie != null) {
|
||||
haveShownLeftEdgeLottie = true
|
||||
illustrationLottie!!.contentDescription =
|
||||
getString(R.string.security_settings_udfps_side_fingerprint_help)
|
||||
configureEnrollmentStage(R.raw.udfps_left_edge_hint_lottie)
|
||||
} else if (illustrationLottie == null) {
|
||||
if (isStageHalfCompleted) {
|
||||
subTitleText.setText(
|
||||
R.string.security_settings_fingerprint_enroll_repeat_message
|
||||
)
|
||||
} else {
|
||||
subTitleText.setText(R.string.security_settings_udfps_enroll_edge_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_RIGHT_EDGE -> {
|
||||
titleText.setText(R.string.security_settings_udfps_enroll_right_edge_title)
|
||||
if (!haveShownRightEdgeLottie && illustrationLottie != null) {
|
||||
haveShownRightEdgeLottie = true
|
||||
illustrationLottie!!.contentDescription =
|
||||
getString(R.string.security_settings_udfps_side_fingerprint_help)
|
||||
configureEnrollmentStage(R.raw.udfps_right_edge_hint_lottie)
|
||||
} else if (illustrationLottie == null) {
|
||||
if (isStageHalfCompleted) {
|
||||
subTitleText.setText(
|
||||
R.string.security_settings_fingerprint_enroll_repeat_message
|
||||
)
|
||||
} else {
|
||||
subTitleText.setText(R.string.security_settings_udfps_enroll_edge_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
STAGE_UNKNOWN -> {
|
||||
titleText.setText(R.string.security_settings_fingerprint_enroll_udfps_title)
|
||||
subTitleText.setText(R.string.security_settings_udfps_enroll_start_message)
|
||||
val description: CharSequence = getString(
|
||||
R.string.security_settings_udfps_enroll_a11y
|
||||
)
|
||||
requireActivity().title = description
|
||||
}
|
||||
|
||||
else -> {
|
||||
titleText.setText(R.string.security_settings_fingerprint_enroll_udfps_title)
|
||||
subTitleText.setText(R.string.security_settings_udfps_enroll_start_message)
|
||||
val description: CharSequence = getString(
|
||||
R.string.security_settings_udfps_enroll_a11y
|
||||
)
|
||||
requireActivity().title = description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateIllustrationLottie(@Surface.Rotation rotation: Int) {
|
||||
if (rotation == ROTATION_90 || rotation == ROTATION_270) {
|
||||
illustrationLottie = null
|
||||
} else if (shouldShowLottie) {
|
||||
illustrationLottie =
|
||||
enrollingView!!.findViewById(R.id.illustration_lottie)
|
||||
}
|
||||
}
|
||||
|
||||
private val currentStage: Int
|
||||
get() {
|
||||
val progress = progressViewModel.progressLiveData.value!!
|
||||
if (progress.steps == -1) {
|
||||
return STAGE_UNKNOWN
|
||||
}
|
||||
val progressSteps: Int = progress.steps - progress.remaining
|
||||
return if (progressSteps < getStageThresholdSteps(0)) {
|
||||
STAGE_CENTER
|
||||
} else if (progressSteps < getStageThresholdSteps(1)) {
|
||||
STAGE_GUIDED
|
||||
} else if (progressSteps < getStageThresholdSteps(2)) {
|
||||
STAGE_FINGERTIP
|
||||
} else if (progressSteps < getStageThresholdSteps(3)) {
|
||||
STAGE_LEFT_EDGE
|
||||
} else {
|
||||
STAGE_RIGHT_EDGE
|
||||
}
|
||||
}
|
||||
|
||||
private val isStageHalfCompleted: Boolean
|
||||
get() {
|
||||
val progress: EnrollmentProgress = progressViewModel.progressLiveData.value!!
|
||||
if (progress.steps == -1) {
|
||||
return false
|
||||
}
|
||||
val progressSteps: Int = progress.steps - progress.remaining
|
||||
var prevThresholdSteps = 0
|
||||
for (i in 0 until enrollingViewModel.getEnrollStageCount()) {
|
||||
val thresholdSteps = getStageThresholdSteps(i)
|
||||
if (progressSteps in prevThresholdSteps until thresholdSteps) {
|
||||
val adjustedProgress = progressSteps - prevThresholdSteps
|
||||
val adjustedThreshold = thresholdSteps - prevThresholdSteps
|
||||
return adjustedProgress >= adjustedThreshold / 2
|
||||
}
|
||||
prevThresholdSteps = thresholdSteps
|
||||
}
|
||||
|
||||
// After last enrollment step.
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getStageThresholdSteps(index: Int): Int {
|
||||
val progress: EnrollmentProgress = progressViewModel.progressLiveData.value!!
|
||||
if (progress.steps == -1) {
|
||||
Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet")
|
||||
return 1
|
||||
}
|
||||
return (progress.steps * enrollingViewModel.getEnrollStageThreshold(index)).roundToInt()
|
||||
}
|
||||
|
||||
private fun configureEnrollmentStage(@RawRes lottie: Int) {
|
||||
subTitleText.text = ""
|
||||
LottieCompositionFactory.fromRawRes(activity, lottie)
|
||||
.addListener { c: LottieComposition ->
|
||||
illustrationLottie?.let {
|
||||
it.setComposition(c)
|
||||
it.visibility = View.VISIBLE
|
||||
it.playAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentProgressChange(progress: EnrollmentProgress) {
|
||||
updateProgress(true /* animate */, progress)
|
||||
updateTitleAndDescription()
|
||||
if (isAccessibilityEnabled) {
|
||||
val steps: Int = progress.steps
|
||||
val remaining: Int = progress.remaining
|
||||
val percent = ((steps - remaining).toFloat() / steps.toFloat() * 100).toInt()
|
||||
val announcement: CharSequence = activity!!.getString(
|
||||
R.string.security_settings_udfps_enroll_progress_a11y_message, percent
|
||||
)
|
||||
enrollingViewModel.sendAccessibilityEvent(announcement)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentHelp(helpMessage: EnrollmentStatusMessage) {
|
||||
Log.d(TAG, "onEnrollmentHelp($helpMessage)")
|
||||
val helpStr: CharSequence = helpMessage.str
|
||||
if (helpStr.isNotEmpty()) {
|
||||
showError(helpStr)
|
||||
udfpsEnrollView.onEnrollmentHelp()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
cancelEnrollment(true)
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "newDialog $errorMessage")
|
||||
errorDialogViewModel.newDialog(errorMessage.msgId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentCanceled(canceledSignal: Any) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
|
||||
)
|
||||
if (enrollingCancelSignal === canceledSignal) {
|
||||
progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
|
||||
progressViewModel.clearProgressLiveData()
|
||||
if (enrollingViewModel.onBackPressed) {
|
||||
enrollingViewModel.onCancelledDueToOnBackPressed()
|
||||
} else if (enrollingViewModel.onSkipPressed) {
|
||||
enrollingViewModel.onCancelledDueToOnSkipPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onAcquired(isAcquiredGood: Boolean) {
|
||||
udfpsEnrollView.onAcquired(isAcquiredGood)
|
||||
}
|
||||
|
||||
private fun onPointerDown(sensorId: Int) {
|
||||
udfpsEnrollView.onPointerDown(sensorId)
|
||||
}
|
||||
|
||||
private fun onPointerUp(sensorId: Int) {
|
||||
udfpsEnrollView.onPointerUp(sensorId)
|
||||
}
|
||||
|
||||
private fun showError(error: CharSequence) {
|
||||
titleText.text = error
|
||||
titleText.contentDescription = error
|
||||
subTitleText.contentDescription = ""
|
||||
}
|
||||
|
||||
private fun onRotationChanged(newRotation: Int) {
|
||||
if ((newRotation + 2) % 4 == rotation) {
|
||||
rotation = newRotation
|
||||
requireContext().configLayout(newRotation, titleText, subTitleText, icon, skipBtn)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = "FingerprintEnrollEnrollingUdfpsFragment"
|
||||
private const val PROGRESS_BAR_MAX = 10000
|
||||
private const val STAGE_UNKNOWN = -1
|
||||
private const val STAGE_CENTER = 0
|
||||
private const val STAGE_GUIDED = 1
|
||||
private const val STAGE_FINGERTIP = 2
|
||||
private const val STAGE_LEFT_EDGE = 3
|
||||
private const val STAGE_RIGHT_EDGE = 4
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollEnrollingUdfpsView(
|
||||
view: RelativeLayout,
|
||||
sensorProperties: FingerprintSensorPropertiesInternal,
|
||||
@Surface.Rotation rotation: Int,
|
||||
onSkipClickListener: View.OnClickListener
|
||||
) {
|
||||
view.findViewById<UdfpsEnrollView>(R.id.udfps_animation_view)!!.setSensorProperties(
|
||||
sensorProperties
|
||||
)
|
||||
|
||||
val titleText = view.findViewById<TextView>(R.id.suc_layout_title)!!
|
||||
val subTitleText = view.findViewById<TextView>(R.id.sud_layout_subtitle)!!
|
||||
val icon = view.findViewById<ImageView>(R.id.sud_layout_icon)!!
|
||||
val skipBtn = view.findViewById<Button>(R.id.skip_btn)!!.also {
|
||||
it.setOnClickListener(onSkipClickListener)
|
||||
}
|
||||
configLayout(rotation, titleText, subTitleText, icon, skipBtn)
|
||||
}
|
||||
|
||||
private fun Context.configLayout(
|
||||
@Surface.Rotation newRotation: Int,
|
||||
titleText: TextView,
|
||||
subTitleText: TextView,
|
||||
icon: ImageView,
|
||||
skipBtn: Button
|
||||
) {
|
||||
if (newRotation == ROTATION_270) {
|
||||
val iconLP = RelativeLayout.LayoutParams(-2, -2)
|
||||
iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||
iconLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view)
|
||||
iconLP.topMargin = convertDpToPixel(76.64f)
|
||||
iconLP.leftMargin = convertDpToPixel(151.54f)
|
||||
icon.layoutParams = iconLP
|
||||
val titleLP = RelativeLayout.LayoutParams(-1, -2)
|
||||
titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||
titleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view)
|
||||
titleLP.topMargin = convertDpToPixel(138f)
|
||||
titleLP.leftMargin = convertDpToPixel(144f)
|
||||
titleText.layoutParams = titleLP
|
||||
val subtitleLP = RelativeLayout.LayoutParams(-1, -2)
|
||||
subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||
subtitleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view)
|
||||
subtitleLP.topMargin = convertDpToPixel(198f)
|
||||
subtitleLP.leftMargin = convertDpToPixel(144f)
|
||||
subTitleText.layoutParams = subtitleLP
|
||||
} else if (newRotation == ROTATION_90) {
|
||||
val metrics = resources.displayMetrics
|
||||
val iconLP = RelativeLayout.LayoutParams(-2, -2)
|
||||
iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||
iconLP.addRule(RelativeLayout.ALIGN_PARENT_START)
|
||||
iconLP.topMargin = convertDpToPixel(76.64f)
|
||||
iconLP.leftMargin = convertDpToPixel(71.99f)
|
||||
icon.layoutParams = iconLP
|
||||
val titleLP = RelativeLayout.LayoutParams(
|
||||
metrics.widthPixels / 2, -2
|
||||
)
|
||||
titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||
titleLP.addRule(RelativeLayout.ALIGN_PARENT_START, R.id.udfps_animation_view)
|
||||
titleLP.topMargin = convertDpToPixel(138f)
|
||||
titleLP.leftMargin = convertDpToPixel(66f)
|
||||
titleText.layoutParams = titleLP
|
||||
val subtitleLP = RelativeLayout.LayoutParams(
|
||||
metrics.widthPixels / 2, -2
|
||||
)
|
||||
subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP)
|
||||
subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_START)
|
||||
subtitleLP.topMargin = convertDpToPixel(198f)
|
||||
subtitleLP.leftMargin = convertDpToPixel(66f)
|
||||
subTitleText.layoutParams = subtitleLP
|
||||
}
|
||||
if (newRotation == ROTATION_90 || newRotation == ROTATION_270) {
|
||||
val skipBtnLP = skipBtn.layoutParams as RelativeLayout.LayoutParams
|
||||
skipBtnLP.topMargin = convertDpToPixel(26f)
|
||||
skipBtnLP.leftMargin = convertDpToPixel(54f)
|
||||
skipBtn.requestLayout()
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.convertDpToPixel(dp: Float): Int {
|
||||
return (dp * resources.displayMetrics.density).toInt()
|
||||
}
|
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.hardware.biometrics.BiometricConstants
|
||||
import android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS
|
||||
import android.hardware.fingerprint.FingerprintManager.FINGERPRINT_ERROR_HW_UNAVAILABLE
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog.getErrorMessage
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog.getErrorTitle
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog.getSetupErrorMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintErrorDialogSetResultAction.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintErrorDialogSetResultAction.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fingerprint error dialog, will be shown when an error occurs during fingerprint enrollment.
|
||||
*/
|
||||
class FingerprintEnrollErrorDialog : DialogFragment() {
|
||||
|
||||
private val viewModel: FingerprintEnrollErrorDialogViewModel?
|
||||
get() = activity?.let {
|
||||
ViewModelProvider(it)[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val errorMsgId: Int = requireArguments().getInt(KEY_ERROR_MSG_ID)
|
||||
val okButtonSetResultAction =
|
||||
if (errorMsgId == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT)
|
||||
FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT
|
||||
else
|
||||
FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH
|
||||
return requireActivity().bindFingerprintEnrollEnrollingErrorDialog(
|
||||
errorMsgId = errorMsgId,
|
||||
isSuw = viewModel!!.isSuw,
|
||||
tryAgainButtonClickListener = { dialog: DialogInterface?, _: Int ->
|
||||
activity?.lifecycleScope?.launch {
|
||||
Log.d(TAG, "tryAgain flow")
|
||||
viewModel?.triggerRetry()
|
||||
dialog?.dismiss()
|
||||
}
|
||||
},
|
||||
okButtonClickListener = { dialog: DialogInterface?, _: Int ->
|
||||
activity?.lifecycleScope?.launch {
|
||||
Log.d(TAG, "ok flow as $okButtonSetResultAction")
|
||||
viewModel?.setResultAndFinish(okButtonSetResultAction)
|
||||
dialog?.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FingerprintEnrollErrorDialog"
|
||||
private const val KEY_ERROR_MSG_ID = "error_msg_id"
|
||||
|
||||
fun newInstance(errorMsgId: Int): FingerprintEnrollErrorDialog {
|
||||
val dialog = FingerprintEnrollErrorDialog()
|
||||
val args = Bundle()
|
||||
args.putInt(KEY_ERROR_MSG_ID, errorMsgId)
|
||||
dialog.arguments = args
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.bindFingerprintEnrollEnrollingErrorDialog(
|
||||
errorMsgId: Int,
|
||||
isSuw: Boolean,
|
||||
tryAgainButtonClickListener: DialogInterface.OnClickListener,
|
||||
okButtonClickListener: DialogInterface.OnClickListener
|
||||
): AlertDialog = AlertDialog.Builder(this)
|
||||
.setTitle(getString(getErrorTitle(errorMsgId)))
|
||||
.setMessage(
|
||||
getString(
|
||||
if (isSuw)
|
||||
getSetupErrorMessage(errorMsgId)
|
||||
else
|
||||
getErrorMessage(errorMsgId)
|
||||
)
|
||||
)
|
||||
.setCancelable(false).apply {
|
||||
if (errorMsgId == FINGERPRINT_ERROR_UNABLE_TO_PROCESS) {
|
||||
setPositiveButton(
|
||||
R.string.security_settings_fingerprint_enroll_dialog_try_again,
|
||||
tryAgainButtonClickListener
|
||||
)
|
||||
setNegativeButton(
|
||||
R.string.security_settings_fingerprint_enroll_dialog_ok,
|
||||
okButtonClickListener
|
||||
)
|
||||
} else {
|
||||
setPositiveButton(
|
||||
R.string.security_settings_fingerprint_enroll_dialog_ok,
|
||||
okButtonClickListener
|
||||
)
|
||||
}
|
||||
}
|
||||
.create()
|
||||
.apply { setCanceledOnTouchOutside(false) }
|
@@ -1,295 +0,0 @@
|
||||
/*
|
||||
* 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 android.content.Context
|
||||
import android.hardware.fingerprint.FingerprintManager.ENROLL_FIND_SENSOR
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
import com.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment explaining the side fingerprint sensor location for fingerprint enrollment.
|
||||
* It interacts with ProgressViewModel, and FingerprintFindSensorAnimation.
|
||||
* <pre>
|
||||
* | Has | UDFPS | SFPS | Other (Rear FPS) |
|
||||
* |---------------------|-------|------|------------------|
|
||||
* | Primary button | Yes | No | No |
|
||||
* | Illustration Lottie | Yes | Yes | No |
|
||||
* | Animation | No | No | Depend on layout |
|
||||
* | Progress ViewModel | No | Yes | Yes |
|
||||
* | Orientation detect | No | Yes | No |
|
||||
* | Foldable detect | No | Yes | No |
|
||||
* </pre>
|
||||
*/
|
||||
class FingerprintEnrollFindRfpsFragment : Fragment() {
|
||||
|
||||
private var _viewModel: FingerprintEnrollFindSensorViewModel? = null
|
||||
private val viewModel: FingerprintEnrollFindSensorViewModel
|
||||
get() = _viewModel!!
|
||||
|
||||
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||
get() = _progressViewModel!!
|
||||
|
||||
private var _rotationViewModel: DeviceRotationViewModel? = null
|
||||
private val rotationViewModel: DeviceRotationViewModel
|
||||
get() = _rotationViewModel!!
|
||||
|
||||
private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
|
||||
private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
|
||||
get() = _errorDialogViewModel!!
|
||||
|
||||
private var findRfpsView: GlifLayout? = null
|
||||
|
||||
private val onSkipClickListener =
|
||||
View.OnClickListener { _: View? -> viewModel.onSkipButtonClick() }
|
||||
|
||||
private var animation: FingerprintFindSensorAnimation? = null
|
||||
|
||||
private var enrollingCancelSignal: Any? = null
|
||||
|
||||
@Surface.Rotation
|
||||
private var lastRotation = -1
|
||||
|
||||
private val progressObserver = Observer { progress: EnrollmentProgress? ->
|
||||
if (progress != null && !progress.isInitialStep) {
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val errorMessageObserver = Observer { errorMessage: EnrollmentStatusMessage? ->
|
||||
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||
errorMessage?.let { onEnrollmentError(it) }
|
||||
}
|
||||
|
||||
private val canceledSignalObserver = Observer { canceledSignal: Any? ->
|
||||
canceledSignal?.let { onEnrollmentCanceled(it) }
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
findRfpsView = inflater.inflate(
|
||||
R.layout.fingerprint_enroll_find_sensor,
|
||||
container,
|
||||
false
|
||||
) as GlifLayout
|
||||
|
||||
val animationView = findRfpsView!!.findViewById<View>(
|
||||
R.id.fingerprint_sensor_location_animation
|
||||
)
|
||||
if (animationView is FingerprintFindSensorAnimation) {
|
||||
animation = animationView
|
||||
}
|
||||
|
||||
return findRfpsView!!
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requireActivity().bindFingerprintEnrollFindRfpsView(
|
||||
view = findRfpsView!!,
|
||||
onSkipClickListener = onSkipClickListener
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.triggerRetryFlow.collect { retryLookingForFingerprint() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retryLookingForFingerprint() {
|
||||
startEnrollment()
|
||||
animation?.let {
|
||||
Log.d(TAG, "retry, start animation")
|
||||
it.startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val isErrorDialogShown = errorDialogViewModel.isDialogShown
|
||||
Log.d(TAG, "onStart(), isEnrolling:${progressViewModel.isEnrolling}"
|
||||
+ ", isErrorDialog:$isErrorDialogShown")
|
||||
if (!isErrorDialogShown) {
|
||||
startEnrollment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
val rotationLiveData: LiveData<Int> = rotationViewModel.liveData
|
||||
lastRotation = rotationLiveData.value!!
|
||||
if (!errorDialogViewModel.isDialogShown) {
|
||||
animation?.let {
|
||||
Log.d(TAG, "onResume(), start animation")
|
||||
it.startAnimation()
|
||||
}
|
||||
}
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
animation?.let {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onPause(), pause animation")
|
||||
}
|
||||
it.pauseAnimation()
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
removeEnrollmentObservers()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isConfigChange = requireActivity().isChangingConfigurations
|
||||
Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
|
||||
if (isEnrolling && !isConfigChange) {
|
||||
cancelEnrollment(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeEnrollmentObservers() {
|
||||
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||
progressViewModel.helpMessageLiveData.removeObserver(errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun startEnrollment() {
|
||||
enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_FIND_SENSOR)
|
||||
if (enrollingCancelSignal == null) {
|
||||
Log.e(TAG, "startEnrollment(), failed to start enrollment")
|
||||
} else {
|
||||
Log.d(TAG, "startEnrollment(), success")
|
||||
}
|
||||
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
|
||||
return
|
||||
}
|
||||
removeEnrollmentObservers()
|
||||
if (waitForLastCancelErrMsg) {
|
||||
progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
|
||||
} else {
|
||||
enrollingCancelSignal = null
|
||||
}
|
||||
val cancelResult: Boolean = progressViewModel.cancelEnrollment()
|
||||
if (!cancelResult) {
|
||||
Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
cancelEnrollment(false)
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "newDialogFlow as $errorMessage")
|
||||
errorDialogViewModel.newDialog(errorMessage.msgId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentCanceled(canceledSignal: Any) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
|
||||
)
|
||||
if (enrollingCancelSignal === canceledSignal) {
|
||||
val progress: EnrollmentProgress? = progressViewModel.progressLiveData.value
|
||||
progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
|
||||
progressViewModel.clearProgressLiveData()
|
||||
if (progress != null && !progress.isInitialStep) {
|
||||
viewModel.onStartButtonClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
animation?.let {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onDestroy(), stop animation")
|
||||
}
|
||||
it.stopAnimation()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
ViewModelProvider(requireActivity()).let { provider ->
|
||||
_viewModel = provider[FingerprintEnrollFindSensorViewModel::class.java]
|
||||
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||
_rotationViewModel = provider[DeviceRotationViewModel::class.java]
|
||||
_errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "FingerprintEnrollFindRfpsFragment"
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollFindRfpsView(
|
||||
view: GlifLayout,
|
||||
onSkipClickListener: View.OnClickListener,
|
||||
) {
|
||||
GlifLayoutHelper(this, view).let {
|
||||
it.setHeaderText(
|
||||
R.string.security_settings_fingerprint_enroll_find_sensor_title
|
||||
)
|
||||
it.setDescriptionText(
|
||||
getText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
|
||||
)
|
||||
}
|
||||
|
||||
view.getMixin(FooterBarMixin::class.java).secondaryButton =
|
||||
FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
.also {
|
||||
it.setOnClickListener(onSkipClickListener)
|
||||
}
|
||||
}
|
@@ -1,319 +0,0 @@
|
||||
/*
|
||||
* 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 android.content.Context
|
||||
import android.hardware.fingerprint.FingerprintManager.ENROLL_FIND_SENSOR
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.RawRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
import com.android.settingslib.widget.LottieColorUtils
|
||||
import com.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fragment explaining the side fingerprint sensor location for fingerprint enrollment.
|
||||
* It interacts with ProgressViewModel, FoldCallback (for different lottie), and
|
||||
* LottieAnimationView.
|
||||
* <pre>
|
||||
* | Has | UDFPS | SFPS | Other (Rear FPS) |
|
||||
* |---------------------|-------|------|------------------|
|
||||
* | Primary button | Yes | No | No |
|
||||
* | Illustration Lottie | Yes | Yes | No |
|
||||
* | Animation | No | No | Depend on layout |
|
||||
* | Progress ViewModel | No | Yes | Yes |
|
||||
* | Orientation detect | No | Yes | No |
|
||||
* | Foldable detect | No | Yes | No |
|
||||
* </pre>
|
||||
*/
|
||||
class FingerprintEnrollFindSfpsFragment : Fragment() {
|
||||
|
||||
private var _viewModel: FingerprintEnrollFindSensorViewModel? = null
|
||||
private val viewModel: FingerprintEnrollFindSensorViewModel
|
||||
get() = _viewModel!!
|
||||
|
||||
private var _progressViewModel: FingerprintEnrollProgressViewModel? = null
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel
|
||||
get() = _progressViewModel!!
|
||||
|
||||
private var _rotationViewModel: DeviceRotationViewModel? = null
|
||||
private val rotationViewModel: DeviceRotationViewModel
|
||||
get() = _rotationViewModel!!
|
||||
|
||||
private var _foldedViewModel: DeviceFoldedViewModel? = null
|
||||
private val foldedViewModel: DeviceFoldedViewModel
|
||||
get() = _foldedViewModel!!
|
||||
|
||||
private var _errorDialogViewModel: FingerprintEnrollErrorDialogViewModel? = null
|
||||
private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel
|
||||
get() = _errorDialogViewModel!!
|
||||
|
||||
private var findSfpsView: GlifLayout? = null
|
||||
|
||||
private val onSkipClickListener =
|
||||
View.OnClickListener { _: View? -> viewModel.onSkipButtonClick() }
|
||||
|
||||
private val illustrationLottie: LottieAnimationView
|
||||
get() = findSfpsView!!.findViewById(R.id.illustration_lottie)!!
|
||||
|
||||
private var enrollingCancelSignal: Any? = null
|
||||
|
||||
@Surface.Rotation
|
||||
private var animationRotation = -1
|
||||
|
||||
private val rotationObserver = Observer { rotation: Int? ->
|
||||
rotation?.let { onRotationChanged(it) }
|
||||
}
|
||||
|
||||
private val progressObserver = Observer { progress: EnrollmentProgress? ->
|
||||
if (progress != null && !progress.isInitialStep) {
|
||||
cancelEnrollment(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val errorMessageObserver = Observer{ errorMessage: EnrollmentStatusMessage? ->
|
||||
Log.d(TAG, "errorMessageObserver($errorMessage)")
|
||||
errorMessage?.let { onEnrollmentError(it) }
|
||||
}
|
||||
|
||||
private val canceledSignalObserver = Observer { canceledSignal: Any? ->
|
||||
canceledSignal?.let { onEnrollmentCanceled(it) }
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = (inflater.inflate(
|
||||
R.layout.sfps_enroll_find_sensor_layout,
|
||||
container,
|
||||
false
|
||||
) as GlifLayout).also {
|
||||
findSfpsView = it
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requireActivity().bindFingerprintEnrollFindSfpsView(
|
||||
view = findSfpsView!!,
|
||||
onSkipClickListener = onSkipClickListener
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.triggerRetryFlow.collect { startEnrollment() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val isErrorDialogShown = errorDialogViewModel.isDialogShown
|
||||
Log.d(TAG, "onStart(), isEnrolling:${progressViewModel.isEnrolling}"
|
||||
+ ", isErrorDialog:$isErrorDialogShown")
|
||||
if (!isErrorDialogShown) {
|
||||
startEnrollment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val rotationLiveData: LiveData<Int> = rotationViewModel.liveData
|
||||
playLottieAnimation(rotationLiveData.value!!)
|
||||
rotationLiveData.observe(this, rotationObserver)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
rotationViewModel.liveData.removeObserver(rotationObserver)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val isEnrolling = progressViewModel.isEnrolling
|
||||
val isConfigChange = requireActivity().isChangingConfigurations
|
||||
Log.d(TAG, "onStop(), enrolling:$isEnrolling isConfigChange:$isConfigChange")
|
||||
if (isEnrolling && !isConfigChange) {
|
||||
cancelEnrollment(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeEnrollmentObservers() {
|
||||
progressViewModel.errorMessageLiveData.removeObserver(errorMessageObserver)
|
||||
progressViewModel.progressLiveData.removeObserver(progressObserver)
|
||||
}
|
||||
|
||||
private fun startEnrollment() {
|
||||
enrollingCancelSignal = progressViewModel.startEnrollment(ENROLL_FIND_SENSOR)
|
||||
if (enrollingCancelSignal == null) {
|
||||
Log.e(TAG, "startEnrollment(), failed to start enrollment")
|
||||
} else {
|
||||
Log.d(TAG, "startEnrollment(), success")
|
||||
}
|
||||
progressViewModel.progressLiveData.observe(this, progressObserver)
|
||||
progressViewModel.errorMessageLiveData.observe(this, errorMessageObserver)
|
||||
}
|
||||
|
||||
private fun cancelEnrollment(waitForLastCancelErrMsg: Boolean) {
|
||||
if (!progressViewModel.isEnrolling) {
|
||||
Log.d(TAG, "cancelEnrollment(), failed because isEnrolling is false")
|
||||
return
|
||||
}
|
||||
removeEnrollmentObservers()
|
||||
if (waitForLastCancelErrMsg) {
|
||||
progressViewModel.canceledSignalLiveData.observe(this, canceledSignalObserver)
|
||||
} else {
|
||||
enrollingCancelSignal = null
|
||||
}
|
||||
val cancelResult: Boolean = progressViewModel.cancelEnrollment()
|
||||
if (!cancelResult) {
|
||||
Log.e(TAG, "cancelEnrollment(), failed to cancel enrollment")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRotationChanged(@Surface.Rotation newRotation: Int) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onRotationChanged() from $animationRotation to $newRotation")
|
||||
}
|
||||
if ((newRotation + 2) % 4 == animationRotation) {
|
||||
// Fragment not changed, we just need to play correct rotation animation
|
||||
playLottieAnimation(newRotation)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentError(errorMessage: EnrollmentStatusMessage) {
|
||||
progressViewModel.cancelEnrollment()
|
||||
lifecycleScope.launch {
|
||||
Log.d(TAG, "newDialogFlow as $errorMessage")
|
||||
errorDialogViewModel.newDialog(errorMessage.msgId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollmentCanceled(canceledSignal: Any) {
|
||||
Log.d(
|
||||
TAG,
|
||||
"onEnrollmentCanceled enrolling:$enrollingCancelSignal, canceled:$canceledSignal"
|
||||
)
|
||||
if (enrollingCancelSignal === canceledSignal) {
|
||||
val progress: EnrollmentProgress? = progressViewModel.progressLiveData.value
|
||||
progressViewModel.canceledSignalLiveData.removeObserver(canceledSignalObserver)
|
||||
progressViewModel.clearProgressLiveData()
|
||||
if (progress != null && !progress.isInitialStep) {
|
||||
viewModel.onStartButtonClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playLottieAnimation(@Surface.Rotation rotation: Int) {
|
||||
@RawRes val animationRawRes = getSfpsLottieAnimationRawRes(rotation)
|
||||
Log.d(
|
||||
TAG,
|
||||
"play lottie animation $animationRawRes, previous rotation:$animationRotation"
|
||||
+ ", new rotation:" + rotation
|
||||
)
|
||||
animationRotation = rotation
|
||||
illustrationLottie.setAnimation(animationRawRes)
|
||||
LottieColorUtils.applyDynamicColors(activity, illustrationLottie)
|
||||
illustrationLottie.visibility = View.VISIBLE
|
||||
illustrationLottie.playAnimation()
|
||||
}
|
||||
|
||||
@RawRes
|
||||
private fun getSfpsLottieAnimationRawRes(@Surface.Rotation rotation: Int): Int {
|
||||
val isFolded = java.lang.Boolean.FALSE != foldedViewModel.liveData.value
|
||||
return when (rotation) {
|
||||
Surface.ROTATION_90 ->
|
||||
if (isFolded)
|
||||
R.raw.fingerprint_edu_lottie_folded_top_left
|
||||
else
|
||||
R.raw.fingerprint_edu_lottie_portrait_top_left
|
||||
Surface.ROTATION_180 ->
|
||||
if (isFolded)
|
||||
R.raw.fingerprint_edu_lottie_folded_bottom_left
|
||||
else
|
||||
R.raw.fingerprint_edu_lottie_landscape_bottom_left
|
||||
Surface.ROTATION_270 ->
|
||||
if (isFolded)
|
||||
R.raw.fingerprint_edu_lottie_folded_bottom_right
|
||||
else
|
||||
R.raw.fingerprint_edu_lottie_portrait_bottom_right
|
||||
else ->
|
||||
if (isFolded)
|
||||
R.raw.fingerprint_edu_lottie_folded_top_right
|
||||
else
|
||||
R.raw.fingerprint_edu_lottie_landscape_top_right
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
ViewModelProvider(requireActivity()).let { provider ->
|
||||
_viewModel = provider[FingerprintEnrollFindSensorViewModel::class.java]
|
||||
_progressViewModel = provider[FingerprintEnrollProgressViewModel::class.java]
|
||||
_rotationViewModel = provider[DeviceRotationViewModel::class.java]
|
||||
_foldedViewModel = provider[DeviceFoldedViewModel::class.java]
|
||||
_errorDialogViewModel = provider[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "FingerprintEnrollFindSfpsFragment"
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollFindSfpsView(
|
||||
view: GlifLayout,
|
||||
onSkipClickListener: View.OnClickListener
|
||||
) {
|
||||
view.getMixin(FooterBarMixin::class.java).let {
|
||||
it.secondaryButton = FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
it.secondaryButton.setOnClickListener(onSkipClickListener)
|
||||
}
|
||||
|
||||
GlifLayoutHelper(this, view).let {
|
||||
it.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
|
||||
it.setDescriptionText(
|
||||
getText(R.string.security_settings_sfps_enroll_find_sensor_message)
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.airbnb.lottie.LottieAnimationView
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
|
||||
import com.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
|
||||
/**
|
||||
* Fragment explaining the under-display fingerprint sensor location for fingerprint enrollment.
|
||||
* It interacts with Primary button, and LottieAnimationView.
|
||||
* <pre>
|
||||
* | Has | UDFPS | SFPS | Other (Rear FPS) |
|
||||
* |---------------------|-------|------|------------------|
|
||||
* | Primary button | Yes | No | No |
|
||||
* | Illustration Lottie | Yes | Yes | No |
|
||||
* | Animation | No | No | Depend on layout |
|
||||
* | Progress ViewModel | No | Yes | Yes |
|
||||
* | Orientation detect | No | Yes | No |
|
||||
* | Foldable detect | No | Yes | No |
|
||||
* </pre>
|
||||
*/
|
||||
class FingerprintEnrollFindUdfpsFragment : Fragment() {
|
||||
|
||||
private var _viewModel: FingerprintEnrollFindSensorViewModel? = null
|
||||
private val mViewModel: FingerprintEnrollFindSensorViewModel
|
||||
get() = _viewModel!!
|
||||
|
||||
private var findUdfpsView: GlifLayout? = null
|
||||
|
||||
private val mOnSkipClickListener =
|
||||
View.OnClickListener { _: View? -> mViewModel.onSkipButtonClick() }
|
||||
|
||||
private val mOnStartClickListener =
|
||||
View.OnClickListener { _: View? -> mViewModel.onStartButtonClick() }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View = (inflater.inflate(
|
||||
R.layout.udfps_enroll_find_sensor_layout,
|
||||
container,
|
||||
false
|
||||
) as GlifLayout).also {
|
||||
findUdfpsView = it
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requireActivity().bindFingerprintEnrollFindUdfpsView(
|
||||
view = findUdfpsView!!,
|
||||
isAccessibilityEnabled = mViewModel.isAccessibilityEnabled,
|
||||
onSkipClickListener = mOnSkipClickListener,
|
||||
onStartClickListener = mOnStartClickListener
|
||||
)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
_viewModel = ViewModelProvider(requireActivity())[
|
||||
FingerprintEnrollFindSensorViewModel::class.java
|
||||
]
|
||||
super.onAttach(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollFindUdfpsView(
|
||||
view: GlifLayout,
|
||||
isAccessibilityEnabled: Boolean,
|
||||
onSkipClickListener: View.OnClickListener,
|
||||
onStartClickListener: View.OnClickListener,
|
||||
) {
|
||||
GlifLayoutHelper(this, view).let { helper ->
|
||||
helper.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title)
|
||||
helper.setDescriptionText(
|
||||
getText(R.string.security_settings_udfps_enroll_find_sensor_message)
|
||||
)
|
||||
}
|
||||
|
||||
view.getMixin(FooterBarMixin::class.java)!!.let {
|
||||
it.secondaryButton = FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
it.secondaryButton.setOnClickListener(onSkipClickListener)
|
||||
|
||||
it.primaryButton = FooterButton.Builder(this)
|
||||
.setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
|
||||
.setButtonType(FooterButton.ButtonType.NEXT)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
|
||||
.build()
|
||||
it.primaryButton.setOnClickListener(onStartClickListener)
|
||||
}
|
||||
|
||||
view.findViewById<LottieAnimationView>(R.id.illustration_lottie)!!.let {
|
||||
it.setOnClickListener(onStartClickListener)
|
||||
if (isAccessibilityEnabled) {
|
||||
it.setAnimation(R.raw.udfps_edu_a11y_lottie)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel
|
||||
import com.google.android.setupcompat.template.FooterBarMixin
|
||||
import com.google.android.setupcompat.template.FooterButton
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
|
||||
/**
|
||||
* Fragment which concludes fingerprint enrollment.
|
||||
*/
|
||||
class FingerprintEnrollFinishFragment : Fragment() {
|
||||
|
||||
private var _viewModel: FingerprintEnrollFinishViewModel? = null
|
||||
private val viewModel: FingerprintEnrollFinishViewModel
|
||||
get() = _viewModel!!
|
||||
|
||||
private val addButtonClickListener =
|
||||
View.OnClickListener { _: View? -> viewModel.onAddButtonClick() }
|
||||
|
||||
private val nextButtonClickListener =
|
||||
View.OnClickListener { _: View? -> viewModel.onNextButtonClick() }
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
_viewModel = ViewModelProvider(requireActivity())[
|
||||
FingerprintEnrollFinishViewModel::class.java
|
||||
]
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) : View = (inflater.inflate(
|
||||
if (viewModel.canAssumeSfps())
|
||||
R.layout.sfps_enroll_finish
|
||||
else
|
||||
R.layout.fingerprint_enroll_finish,
|
||||
container,
|
||||
false
|
||||
) as GlifLayout).also {
|
||||
requireActivity().bindFingerprintEnrollFinishFragment(
|
||||
view = it,
|
||||
isSuw = viewModel.request.isSuw,
|
||||
canAssumeSfps = viewModel.canAssumeSfps(),
|
||||
isAnotherFingerprintEnrollable = viewModel.isAnotherFingerprintEnrollable,
|
||||
nextButtonClickListener = nextButtonClickListener,
|
||||
addButtonClickListener = addButtonClickListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollFinishFragment(
|
||||
view: GlifLayout,
|
||||
isSuw: Boolean,
|
||||
canAssumeSfps: Boolean,
|
||||
isAnotherFingerprintEnrollable: Boolean,
|
||||
nextButtonClickListener: View.OnClickListener,
|
||||
addButtonClickListener: View.OnClickListener
|
||||
) {
|
||||
GlifLayoutHelper(this, view).apply {
|
||||
setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title)
|
||||
setDescriptionText(
|
||||
getString(
|
||||
if (canAssumeSfps && isAnotherFingerprintEnrollable)
|
||||
R.string.security_settings_fingerprint_enroll_finish_v2_add_fingerprint_message
|
||||
else
|
||||
R.string.security_settings_fingerprint_enroll_finish_v2_message
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
view.getMixin(FooterBarMixin::class.java).also { footer ->
|
||||
footer.primaryButton = FooterButton.Builder(this)
|
||||
.setText(
|
||||
if (isSuw)
|
||||
R.string.next_label
|
||||
else
|
||||
R.string.security_settings_fingerprint_enroll_done
|
||||
)
|
||||
.setListener(nextButtonClickListener)
|
||||
.setButtonType(FooterButton.ButtonType.NEXT)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
|
||||
.build()
|
||||
if (isAnotherFingerprintEnrollable) {
|
||||
footer.secondaryButton = FooterButton.Builder(this)
|
||||
.setText(R.string.fingerprint_enroll_button_add)
|
||||
.setListener(addButtonClickListener)
|
||||
.setButtonType(FooterButton.ButtonType.SKIP)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,329 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED
|
||||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
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.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN
|
||||
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.DeviceHelper
|
||||
import com.google.android.setupdesign.util.DynamicColorPalette
|
||||
import com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT
|
||||
import java.util.function.Supplier
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fingerprint intro onboarding page fragment implementation
|
||||
*/
|
||||
class FingerprintEnrollIntroFragment : Fragment() {
|
||||
|
||||
private val viewModelProvider: ViewModelProvider
|
||||
get() = ViewModelProvider(requireActivity())
|
||||
|
||||
private var _viewModel: FingerprintEnrollIntroViewModel? = null
|
||||
private val viewModel: FingerprintEnrollIntroViewModel
|
||||
get() = _viewModel!!
|
||||
|
||||
private var introView: GlifLayout? = null
|
||||
|
||||
private var primaryFooterButton: FooterButton? = null
|
||||
|
||||
private var secondaryFooterButton: FooterButton? = null
|
||||
|
||||
private val onNextClickListener =
|
||||
View.OnClickListener { _: View? ->
|
||||
activity?.lifecycleScope?.let {
|
||||
viewModel.onNextButtonClick(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val onSkipOrCancelClickListener =
|
||||
View.OnClickListener { _: View? ->
|
||||
activity?.lifecycleScope?.let {
|
||||
viewModel.onSkipOrCancelButtonClick(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
introView = inflater.inflate(
|
||||
R.layout.fingerprint_enroll_introduction,
|
||||
container,
|
||||
false
|
||||
) as GlifLayout
|
||||
return introView!!
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requireActivity().bindFingerprintEnrollIntroView(
|
||||
view = introView!!,
|
||||
canAssumeUdfps = viewModel.canAssumeUdfps,
|
||||
isBiometricUnlockDisabledByAdmin = viewModel.isBiometricUnlockDisabledByAdmin,
|
||||
isParentalConsentRequired = viewModel.isParentalConsentRequired,
|
||||
descriptionDisabledByAdminSupplier = { getDescriptionDisabledByAdmin(view.context) }
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
val context: Context = requireContext()
|
||||
val footerBarMixin: FooterBarMixin = footerBarMixin
|
||||
viewModel.updateEnrollableStatus(lifecycleScope)
|
||||
initPrimaryFooterButton(context, footerBarMixin)
|
||||
initSecondaryFooterButton(context, footerBarMixin)
|
||||
collectPageStatusFlowIfNeed()
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
private fun initPrimaryFooterButton(
|
||||
context: Context,
|
||||
footerBarMixin: FooterBarMixin
|
||||
) {
|
||||
if (footerBarMixin.primaryButton != null) {
|
||||
return
|
||||
}
|
||||
primaryFooterButton = FooterButton.Builder(context)
|
||||
.setText(R.string.security_settings_fingerprint_enroll_introduction_agree)
|
||||
.setButtonType(FooterButton.ButtonType.OPT_IN)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
|
||||
.build()
|
||||
.also {
|
||||
it.setOnClickListener(onNextClickListener)
|
||||
footerBarMixin.primaryButton = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSecondaryFooterButton(
|
||||
context: Context,
|
||||
footerBarMixin: FooterBarMixin
|
||||
) {
|
||||
if (footerBarMixin.secondaryButton != null) {
|
||||
return
|
||||
}
|
||||
secondaryFooterButton = FooterButton.Builder(context)
|
||||
.setText(
|
||||
if (viewModel.request.isAfterSuwOrSuwSuggestedAction)
|
||||
R.string.security_settings_fingerprint_enroll_introduction_cancel
|
||||
else
|
||||
R.string.security_settings_fingerprint_enroll_introduction_no_thanks
|
||||
)
|
||||
.setButtonType(FooterButton.ButtonType.NEXT)
|
||||
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
|
||||
.build()
|
||||
.also {
|
||||
it.setOnClickListener(onSkipOrCancelClickListener)
|
||||
footerBarMixin.setSecondaryButton(it, true /* usePrimaryStyle */)
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectPageStatusFlowIfNeed() {
|
||||
lifecycleScope.launch {
|
||||
val status = viewModel.pageStatusFlow.first()
|
||||
Log.d(TAG, "collectPageStatusFlowIfNeed status:$status")
|
||||
if (status.hasScrollToBottom()
|
||||
|| status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
) {
|
||||
// Update once and do not requireScrollWithButton() again when page has
|
||||
// scrolled to bottom or User has enrolled at least a fingerprint, because if
|
||||
// we requireScrollWithButton() again, primary button will become "More" after
|
||||
// scrolling.
|
||||
updateFooterButtons(status)
|
||||
} else {
|
||||
introView!!.getMixin(RequireScrollMixin::class.java).let {
|
||||
it.requireScrollWithButton(
|
||||
requireActivity(),
|
||||
primaryFooterButton!!,
|
||||
moreButtonTextRes,
|
||||
onNextClickListener
|
||||
)
|
||||
it.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean ->
|
||||
viewModel.setHasScrolledToBottom(!scrollNeeded, lifecycleScope)
|
||||
}
|
||||
}
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.pageStatusFlow.collect(
|
||||
this@FingerprintEnrollIntroFragment::updateFooterButtons
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
_viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
private val footerBarMixin: FooterBarMixin
|
||||
get() = introView!!.getMixin(FooterBarMixin::class.java)
|
||||
|
||||
private fun getDescriptionDisabledByAdmin(context: Context): String? {
|
||||
val defaultStrId: Int =
|
||||
R.string.security_settings_fingerprint_enroll_introduction_message_unlock_disabled
|
||||
val devicePolicyManager: DevicePolicyManager =
|
||||
checkNotNull(requireActivity().getSystemService(DevicePolicyManager::class.java))
|
||||
|
||||
return devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED) {
|
||||
context.getString(defaultStrId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFooterButtons(status: FingerprintEnrollIntroStatus) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateFooterButtons($status)")
|
||||
}
|
||||
primaryFooterButton!!.setText(
|
||||
context,
|
||||
if (status.enrollableStatus === FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
|
||||
R.string.done
|
||||
else if (status.hasScrollToBottom())
|
||||
R.string.security_settings_fingerprint_enroll_introduction_agree
|
||||
else
|
||||
moreButtonTextRes
|
||||
)
|
||||
secondaryFooterButton!!.visibility =
|
||||
if (status.hasScrollToBottom()
|
||||
&& status.enrollableStatus !== FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
)
|
||||
View.VISIBLE
|
||||
else
|
||||
View.INVISIBLE
|
||||
|
||||
view!!.requireViewById<TextView>(R.id.error_text).let {
|
||||
when (status.enrollableStatus) {
|
||||
FINGERPRINT_ENROLLABLE_OK -> {
|
||||
it.text = null
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX -> {
|
||||
it.setText(R.string.fingerprint_intro_error_max)
|
||||
it.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLLABLE_UNKNOWN -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get:StringRes
|
||||
private val moreButtonTextRes: Int
|
||||
get() = R.string.security_settings_face_enroll_introduction_more
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FingerprintEnrollIntroFragment"
|
||||
private const val DEBUG = false
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.bindFingerprintEnrollIntroView(
|
||||
view: GlifLayout,
|
||||
canAssumeUdfps: Boolean,
|
||||
isBiometricUnlockDisabledByAdmin: Boolean,
|
||||
isParentalConsentRequired: Boolean,
|
||||
descriptionDisabledByAdminSupplier: Supplier<String?>
|
||||
) {
|
||||
val context = view.context
|
||||
|
||||
val iconFingerprint = view.findViewById<ImageView>(R.id.icon_fingerprint)!!
|
||||
val iconDeviceLocked = view.findViewById<ImageView>(R.id.icon_device_locked)!!
|
||||
val iconTrashCan = view.findViewById<ImageView>(R.id.icon_trash_can)!!
|
||||
val iconInfo = view.findViewById<ImageView>(R.id.icon_info)!!
|
||||
val iconShield = view.findViewById<ImageView>(R.id.icon_shield)!!
|
||||
val iconLink = view.findViewById<ImageView>(R.id.icon_link)!!
|
||||
val footerMessage6 = view.findViewById<TextView>(R.id.footer_message_6)!!
|
||||
|
||||
PorterDuffColorFilter(
|
||||
DynamicColorPalette.getColor(context, ACCENT),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
).let { colorFilter ->
|
||||
iconFingerprint.drawable.colorFilter = colorFilter
|
||||
iconDeviceLocked.drawable.colorFilter = colorFilter
|
||||
iconTrashCan.drawable.colorFilter = colorFilter
|
||||
iconInfo.drawable.colorFilter = colorFilter
|
||||
iconShield.drawable.colorFilter = colorFilter
|
||||
iconLink.drawable.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
view.findViewById<TextView>(R.id.footer_learn_more)!!.let { learnMore ->
|
||||
learnMore.movementMethod = LinkMovementMethod.getInstance()
|
||||
val footerLinkStr: String = context.getString(
|
||||
R.string.security_settings_fingerprint_v2_enroll_introduction_message_learn_more,
|
||||
Html.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
learnMore.text = Html.fromHtml(footerLinkStr)
|
||||
}
|
||||
|
||||
if (canAssumeUdfps) {
|
||||
footerMessage6.visibility = View.VISIBLE
|
||||
iconShield.visibility = View.VISIBLE
|
||||
} else {
|
||||
footerMessage6.visibility = View.GONE
|
||||
iconShield.visibility = View.GONE
|
||||
}
|
||||
val glifLayoutHelper = GlifLayoutHelper(this, view)
|
||||
if (isBiometricUnlockDisabledByAdmin && !isParentalConsentRequired) {
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled
|
||||
)
|
||||
glifLayoutHelper.setDescriptionText(descriptionDisabledByAdminSupplier.get())
|
||||
} else {
|
||||
glifLayoutHelper.setHeaderText(
|
||||
R.string.security_settings_fingerprint_enroll_introduction_title
|
||||
)
|
||||
glifLayoutHelper.setDescriptionText(
|
||||
getString(
|
||||
R.string.security_settings_fingerprint_enroll_introduction_v3_message,
|
||||
DeviceHelper.getDeviceName(context)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<ScrollView>(com.google.android.setupdesign.R.id.sud_scroll_view)
|
||||
?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||
}
|
@@ -1,633 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.view
|
||||
|
||||
import android.annotation.StyleRes
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources.Theme
|
||||
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.ActivityResultCallback
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
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.biometrics2.factory.BiometricsViewModelFactory
|
||||
import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR_KEY
|
||||
import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CREDENTIAL_MODEL_KEY
|
||||
import com.android.settings.biometrics2.factory.BiometricsViewModelFactory.ENROLLMENT_REQUEST_KEY
|
||||
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.CredentialAction
|
||||
import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_BACK_PRESSED
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FingerprintEnrollEnrollingAction
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollErrorDialogViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FingerprintEnrollFindSensorAction
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FingerprintEnrollFinishAction
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintErrorDialogSetResultAction.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintErrorDialogSetResultAction.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT
|
||||
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
|
||||
import com.google.android.setupdesign.util.ThemeHelper
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fingerprint enrollment activity implementation
|
||||
*/
|
||||
open class FingerprintEnrollmentActivity : FragmentActivity() {
|
||||
/** SetupWizard activity*/
|
||||
class SetupActivity : FingerprintEnrollmentActivity()
|
||||
|
||||
/** Internal activity for FingerprintSettings */
|
||||
class InternalActivity : FingerprintEnrollmentActivity()
|
||||
|
||||
private val viewModelProvider: ViewModelProvider by lazy {
|
||||
ViewModelProvider(this)
|
||||
}
|
||||
|
||||
private val viewModel: FingerprintEnrollmentViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollmentViewModel::class.java]
|
||||
}
|
||||
|
||||
private val autoCredentialViewModel: AutoCredentialViewModel by lazy {
|
||||
viewModelProvider[AutoCredentialViewModel::class.java]
|
||||
}
|
||||
|
||||
private val introViewModel: FingerprintEnrollIntroViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
|
||||
}
|
||||
|
||||
private val findSensorViewModel: FingerprintEnrollFindSensorViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
|
||||
}
|
||||
|
||||
private val progressViewModel: FingerprintEnrollProgressViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollProgressViewModel::class.java]
|
||||
}
|
||||
|
||||
private val enrollingViewModel: FingerprintEnrollEnrollingViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollEnrollingViewModel::class.java]
|
||||
}
|
||||
|
||||
private val finishViewModel: FingerprintEnrollFinishViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollFinishViewModel::class.java]
|
||||
}
|
||||
|
||||
private val errorDialogViewModel: FingerprintEnrollErrorDialogViewModel by lazy {
|
||||
viewModelProvider[FingerprintEnrollErrorDialogViewModel::class.java]
|
||||
}
|
||||
|
||||
private var isFirstFragmentAdded = false
|
||||
|
||||
private val findSensorActionObserver = Observer<Int?> { action ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "findSensorActionObserver($action)")
|
||||
}
|
||||
action?.let { onFindSensorAction(it) }
|
||||
}
|
||||
|
||||
private val enrollingActionObserver = Observer<Int?> { action ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "enrollingActionObserver($action)")
|
||||
}
|
||||
action?.let { onEnrollingAction(it) }
|
||||
}
|
||||
|
||||
private val finishActionObserver = Observer<Int?> { action ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "finishActionObserver($action)")
|
||||
}
|
||||
action?.let { onFinishAction(it) }
|
||||
}
|
||||
|
||||
private val chooseLockResultCallback: ActivityResultCallback<ActivityResult> =
|
||||
ActivityResultCallback { result ->
|
||||
onChooseOrConfirmLockResult(true /* isChooseLock */, result)
|
||||
}
|
||||
|
||||
private val chooseLockLauncher: ActivityResultLauncher<Intent> =
|
||||
registerForActivityResult(StartActivityForResult(), chooseLockResultCallback)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Theme
|
||||
setTheme(viewModel.request.theme)
|
||||
ThemeHelper.trySetDynamicColor(this)
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
|
||||
// fragment
|
||||
setContentView(R.layout.biometric_enrollment_container)
|
||||
val fragment: Fragment? = supportFragmentManager.findFragmentById(
|
||||
R.id.fragment_container_view
|
||||
)
|
||||
Log.d(
|
||||
TAG,
|
||||
"onCreate() has savedInstance:$(savedInstanceState != null), fragment:$fragment"
|
||||
)
|
||||
|
||||
isFirstFragmentAdded = (savedInstanceState != null)
|
||||
if (fragment == null) {
|
||||
checkCredential()
|
||||
if (viewModel.request.isSkipFindSensor) {
|
||||
startEnrollingFragment()
|
||||
} else if (viewModel.request.isSkipIntro) {
|
||||
startFindSensorFragment()
|
||||
} else {
|
||||
startIntroFragment()
|
||||
}
|
||||
} else {
|
||||
val tag: String? = fragment.tag
|
||||
if (INTRO_TAG == tag) {
|
||||
attachIntroViewModel()
|
||||
} else if (FIND_SENSOR_TAG == tag) {
|
||||
attachFindSensorViewModel()
|
||||
attachIntroViewModel()
|
||||
} else if (ENROLLING_TAG == tag) {
|
||||
attachEnrollingViewModel()
|
||||
attachFindSensorViewModel()
|
||||
attachIntroViewModel()
|
||||
} else if (FINISH_TAG == tag) {
|
||||
attachFinishViewModel()
|
||||
attachFindSensorViewModel()
|
||||
attachIntroViewModel()
|
||||
} else {
|
||||
Log.e(TAG, "fragment tag $tag not found")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
collectFlows()
|
||||
}
|
||||
|
||||
private fun collectFlows() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.setResultFlow.collect {
|
||||
Log.d(TAG, "setResultLiveData($it)")
|
||||
onSetActivityResult(it)
|
||||
}
|
||||
}
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
autoCredentialViewModel.generateChallengeFailedFlow.collect {
|
||||
Log.d(TAG, "generateChallengeFailedFlow($it)")
|
||||
onSetActivityResult(ActivityResult(RESULT_CANCELED, null))
|
||||
}
|
||||
}
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.newDialogFlow.collect {
|
||||
Log.d(TAG, "newErrorDialogFlow($it)")
|
||||
FingerprintEnrollErrorDialog.newInstance(it).show(
|
||||
supportFragmentManager,
|
||||
ERROR_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
errorDialogViewModel.setResultFlow.collect {
|
||||
Log.d(TAG, "errorDialogSetResultFlow($it)")
|
||||
when (it) {
|
||||
FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH -> onSetActivityResult(
|
||||
ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null)
|
||||
)
|
||||
|
||||
FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT -> onSetActivityResult(
|
||||
ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFragment(fragmentClass: Class<out Fragment>, tag: String) {
|
||||
if (!isFirstFragmentAdded) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.setReorderingAllowed(true)
|
||||
.replace(R.id.fragment_container_view, fragmentClass, null, tag)
|
||||
.commit()
|
||||
isFirstFragmentAdded = true
|
||||
} else {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.setReorderingAllowed(true)
|
||||
.setCustomAnimations(
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_open_enter_dynamic_color,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_open_exit,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_close_enter_dynamic_color,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_close_exit
|
||||
)
|
||||
.replace(R.id.fragment_container_view, fragmentClass, null, tag)
|
||||
.addToBackStack(tag)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startIntroFragment() {
|
||||
attachIntroViewModel()
|
||||
startFragment(FingerprintEnrollIntroFragment::class.java, INTRO_TAG)
|
||||
}
|
||||
|
||||
private fun attachIntroViewModel() {
|
||||
val request: EnrollmentRequest = viewModel.request
|
||||
if (request.isSkipIntro || request.isSkipFindSensor) {
|
||||
return
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
introViewModel.actionFlow.collect(this@FingerprintEnrollmentActivity::onIntroAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to make sure token is valid before entering find sensor page
|
||||
private fun startFindSensorFragment() {
|
||||
// Always setToken into progressViewModel even it is not necessary action for UDFPS
|
||||
progressViewModel.setToken(autoCredentialViewModel.token)
|
||||
attachFindSensorViewModel()
|
||||
val fragmentClass: Class<out Fragment> = if (viewModel.canAssumeUdfps) {
|
||||
FingerprintEnrollFindUdfpsFragment::class.java
|
||||
} else if (viewModel.canAssumeSfps) {
|
||||
FingerprintEnrollFindSfpsFragment::class.java
|
||||
} else {
|
||||
FingerprintEnrollFindRfpsFragment::class.java
|
||||
}
|
||||
startFragment(fragmentClass, FIND_SENSOR_TAG)
|
||||
}
|
||||
|
||||
private fun attachFindSensorViewModel() {
|
||||
if (viewModel.request.isSkipFindSensor) {
|
||||
return
|
||||
}
|
||||
findSensorViewModel.let {
|
||||
// Clear ActionLiveData in FragmentViewModel to prevent getting previous action during
|
||||
// recreate, like press 'Start' then press 'back' in FingerprintEnrollEnrolling
|
||||
// activity.
|
||||
it.clearActionLiveData()
|
||||
it.actionLiveData.observe(this, findSensorActionObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startEnrollingFragment() {
|
||||
// Always setToken into progressViewModel even it is not necessary action for SFPS or RFPS
|
||||
progressViewModel.setToken(autoCredentialViewModel.token)
|
||||
attachEnrollingViewModel()
|
||||
val fragmentClass: Class<out Fragment> = if (viewModel.canAssumeUdfps) {
|
||||
FingerprintEnrollEnrollingUdfpsFragment::class.java
|
||||
} else if (viewModel.canAssumeSfps) {
|
||||
FingerprintEnrollEnrollingSfpsFragment::class.java
|
||||
} else {
|
||||
FingerprintEnrollEnrollingRfpsFragment::class.java
|
||||
}
|
||||
startFragment(fragmentClass, ENROLLING_TAG)
|
||||
}
|
||||
|
||||
private fun attachEnrollingViewModel() {
|
||||
enrollingViewModel.let {
|
||||
it.clearActionLiveData()
|
||||
it.actionLiveData.observe(this, enrollingActionObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFinishFragment() {
|
||||
viewModel.isNewFingerprintAdded = true
|
||||
attachFinishViewModel()
|
||||
if (viewModel.request.isSkipFindSensor) {
|
||||
// Set page to Finish
|
||||
supportFragmentManager.beginTransaction()
|
||||
.setReorderingAllowed(true)
|
||||
.setCustomAnimations(
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_open_enter_dynamic_color,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_open_exit,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_close_enter_dynamic_color,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_close_exit
|
||||
)
|
||||
.replace(
|
||||
R.id.fragment_container_view,
|
||||
FingerprintEnrollFinishFragment::class.java,
|
||||
null,
|
||||
FINISH_TAG
|
||||
)
|
||||
.commit()
|
||||
} else {
|
||||
// Remove Enrolling page
|
||||
supportFragmentManager.popBackStack()
|
||||
|
||||
// Remove old Finish page if any
|
||||
if (supportFragmentManager.findFragmentByTag(FINISH_TAG) != null) {
|
||||
supportFragmentManager.popBackStack(FINISH_TAG, POP_BACK_STACK_INCLUSIVE)
|
||||
}
|
||||
|
||||
// Remove FindSensor page if maxEnrolled
|
||||
if (viewModel.isMaxEnrolledReached(autoCredentialViewModel.userId)
|
||||
&& supportFragmentManager.findFragmentByTag(FIND_SENSOR_TAG) != null
|
||||
) {
|
||||
supportFragmentManager.popBackStack(FIND_SENSOR_TAG, POP_BACK_STACK_INCLUSIVE)
|
||||
}
|
||||
|
||||
// Add Finish page
|
||||
supportFragmentManager.beginTransaction()
|
||||
.setReorderingAllowed(true)
|
||||
.setCustomAnimations(
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_open_enter_dynamic_color,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_open_exit,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_close_enter_dynamic_color,
|
||||
com.google.android.setupdesign.R.anim.shared_x_axis_activity_close_exit
|
||||
)
|
||||
.replace(
|
||||
R.id.fragment_container_view,
|
||||
FingerprintEnrollFinishFragment::class.java,
|
||||
null,
|
||||
FINISH_TAG
|
||||
)
|
||||
.addToBackStack(FINISH_TAG)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachFinishViewModel() {
|
||||
finishViewModel.let {
|
||||
it.clearActionLiveData()
|
||||
it.actionLiveData.observe(this, finishActionObserver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSetActivityResult(result: ActivityResult) {
|
||||
val challengeExtras: Bundle? = autoCredentialViewModel.createGeneratingChallengeExtras()
|
||||
val overrideResult: ActivityResult = viewModel.getOverrideActivityResult(
|
||||
result, challengeExtras
|
||||
)
|
||||
if (DEBUG) {
|
||||
Log.d(
|
||||
TAG, "onSetActivityResult(" + result + "), override:" + overrideResult
|
||||
+ ") challengeExtras:" + challengeExtras
|
||||
)
|
||||
}
|
||||
setResult(overrideResult.resultCode, overrideResult.data)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun checkCredential() {
|
||||
when (autoCredentialViewModel.checkCredential(lifecycleScope)) {
|
||||
CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK -> {
|
||||
val intent: Intent = autoCredentialViewModel.createChooseLockIntent(
|
||||
this,
|
||||
viewModel.request.isSuw,
|
||||
viewModel.request.suwExtras
|
||||
)
|
||||
if (!viewModel.isWaitingActivityResult.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "chooseLock, fail to set isWaiting flag to true")
|
||||
}
|
||||
chooseLockLauncher.launch(intent)
|
||||
return
|
||||
}
|
||||
|
||||
CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK -> {
|
||||
val launched: Boolean = autoCredentialViewModel.createConfirmLockLauncher(
|
||||
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 (!viewModel.isWaitingActivityResult.compareAndSet(false, true)) {
|
||||
Log.w(TAG, "confirmLock, fail to set isWaiting flag to true")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
CredentialAction.CREDENTIAL_VALID,
|
||||
CredentialAction.IS_GENERATING_CHALLENGE -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onChooseOrConfirmLockResult(
|
||||
isChooseLock: Boolean,
|
||||
activityResult: ActivityResult
|
||||
) {
|
||||
if (!viewModel.isWaitingActivityResult.compareAndSet(true, false)) {
|
||||
Log.w(TAG, "isChooseLock:$isChooseLock, fail to unset waiting flag")
|
||||
}
|
||||
if (!autoCredentialViewModel.generateChallengeAsCredentialActivityResult(
|
||||
isChooseLock,
|
||||
activityResult,
|
||||
lifecycleScope
|
||||
)
|
||||
) {
|
||||
onSetActivityResult(activityResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIntroAction(action: FingerprintEnrollIntroAction) {
|
||||
Log.d(TAG, "onIntroAction($action)")
|
||||
when (action) {
|
||||
FingerprintEnrollIntroAction.DONE_AND_FINISH -> {
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null))
|
||||
return
|
||||
}
|
||||
|
||||
FingerprintEnrollIntroAction.SKIP_OR_CANCEL -> {
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_SKIP, null))
|
||||
return
|
||||
}
|
||||
|
||||
FingerprintEnrollIntroAction.CONTINUE_ENROLL -> {
|
||||
startFindSensorFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFindSensorAction(@FingerprintEnrollFindSensorAction action: Int) {
|
||||
when (action) {
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP -> {
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_SKIP, null))
|
||||
return
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG -> {
|
||||
SkipSetupFindFpsDialog().show(
|
||||
supportFragmentManager,
|
||||
SKIP_SETUP_FIND_FPS_DIALOG_TAG
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START -> {
|
||||
startEnrollingFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnrollingAction(@FingerprintEnrollEnrollingAction action: Int) {
|
||||
when (action) {
|
||||
FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE -> {
|
||||
startFinishFragment()
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP -> {
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_SKIP, null))
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG -> {
|
||||
FingerprintEnrollEnrollingIconTouchDialog().show(
|
||||
supportFragmentManager,
|
||||
SKIP_SETUP_FIND_FPS_DIALOG_TAG
|
||||
)
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_BACK_PRESSED -> {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
} else {
|
||||
onSetActivityResult(ActivityResult(RESULT_CANCELED, null))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFinishAction(@FingerprintEnrollFinishAction action: Int) {
|
||||
when (action) {
|
||||
FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK -> {
|
||||
startEnrollingFragment()
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK -> {
|
||||
val data: Intent? = if (viewModel.request.isSuw) {
|
||||
Intent().also {
|
||||
it.putExtras(
|
||||
viewModel.getSuwFingerprintCountExtra(
|
||||
autoCredentialViewModel.userId
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_FINISHED, data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
viewModel.checkFinishActivityDuringOnPause(
|
||||
isFinishing,
|
||||
isChangingConfigurations,
|
||||
lifecycleScope
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
viewModel.updateFingerprintSuggestionEnableState(autoCredentialViewModel.userId)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onApplyThemeResource(theme: Theme, @StyleRes resid: Int, first: Boolean) {
|
||||
theme.applyStyle(R.style.SetupWizardPartnerResource, true)
|
||||
super.onApplyThemeResource(theme, resid, first)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) {
|
||||
onChooseOrConfirmLockResult(false, ActivityResult(resultCode, data))
|
||||
return
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override val defaultViewModelCreationExtras: CreationExtras
|
||||
get() = MutableCreationExtras(super.defaultViewModelCreationExtras).also {
|
||||
it[CHALLENGE_GENERATOR_KEY] = FingerprintChallengeGenerator(
|
||||
featureFactory.biometricsRepositoryProvider.getFingerprintRepository(application)!!
|
||||
)
|
||||
it[ENROLLMENT_REQUEST_KEY] =
|
||||
EnrollmentRequest(intent, applicationContext, this is SetupActivity)
|
||||
it[CREDENTIAL_MODEL_KEY] =
|
||||
CredentialModel(intent.extras, SystemClock.elapsedRealtimeClock())
|
||||
}
|
||||
|
||||
override val defaultViewModelProviderFactory: ViewModelProvider.Factory
|
||||
get() = BiometricsViewModelFactory()
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
window.statusBarColor = backgroundColor
|
||||
}
|
||||
|
||||
@get:ColorInt
|
||||
private val backgroundColor: Int
|
||||
get() {
|
||||
val stateList: ColorStateList? =
|
||||
Utils.getColorAttr(this, android.R.attr.windowBackground)
|
||||
return stateList?.defaultColor ?: Color.TRANSPARENT
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
viewModelProvider[DeviceFoldedViewModel::class.java].onConfigurationChanged(newConfig)
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG = false
|
||||
private const val TAG = "FingerprintEnrollmentActivity"
|
||||
protected const val LAUNCH_CONFIRM_LOCK_ACTIVITY = 1
|
||||
|
||||
private const val INTRO_TAG = "intro"
|
||||
private const val FIND_SENSOR_TAG = "find-sensor"
|
||||
private const val ENROLLING_TAG = "enrolling"
|
||||
private const val FINISH_TAG = "finish"
|
||||
private const val SKIP_SETUP_FIND_FPS_DIALOG_TAG = "skip-setup-dialog"
|
||||
private const val ERROR_DIALOG_TAG = "error-dialog"
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* 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 android.app.Activity
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.google.android.setupdesign.GlifLayout
|
||||
|
||||
/**
|
||||
* Utils class for GlifLayout
|
||||
*/
|
||||
class GlifLayoutHelper(val activity: Activity, val glifLayout: GlifLayout) {
|
||||
|
||||
/**
|
||||
* Sets header text to GlifLayout
|
||||
*/
|
||||
fun setHeaderText(@StringRes textResId: Int) {
|
||||
val layoutTitle = glifLayout.headerTextView
|
||||
val previousTitle = layoutTitle.text
|
||||
val title = activity.getText(textResId)
|
||||
if (previousTitle !== title) {
|
||||
if (!TextUtils.isEmpty(previousTitle)) {
|
||||
layoutTitle.accessibilityLiveRegion = View.ACCESSIBILITY_LIVE_REGION_POLITE
|
||||
}
|
||||
glifLayout.headerText = title
|
||||
glifLayout.headerTextView.contentDescription = title
|
||||
activity.title = title
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets description text to GlifLayout
|
||||
*/
|
||||
fun setDescriptionText(description: CharSequence?) {
|
||||
val previousDescription = glifLayout.descriptionText
|
||||
// Prevent a11y for re-reading the same string
|
||||
if (!TextUtils.equals(previousDescription, description)) {
|
||||
glifLayout.descriptionText = description
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* 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 android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.android.settings.R
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel
|
||||
|
||||
/**
|
||||
* Skip dialog which shows when user clicks "Do it later" button in FingerprintFindSensor page.
|
||||
*/
|
||||
class SkipSetupFindFpsDialog : DialogFragment() {
|
||||
|
||||
private var mViewModel: FingerprintEnrollFindSensorViewModel? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
|
||||
requireActivity().bindSkipSetupFindFpsDialog {
|
||||
_: DialogInterface?, _: Int -> mViewModel?.onSkipDialogButtonClick()
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
mViewModel = ViewModelProvider(requireActivity())[
|
||||
FingerprintEnrollFindSensorViewModel::class.java
|
||||
]
|
||||
super.onAttach(context)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.bindSkipSetupFindFpsDialog(
|
||||
positiveButtonClickListener: DialogInterface.OnClickListener
|
||||
): AlertDialog =
|
||||
AlertDialog.Builder(this, R.style.Theme_AlertDialog)
|
||||
.setTitle(R.string.setup_fingerprint_enroll_skip_title)
|
||||
.setPositiveButton(R.string.skip_anyway_button_label, positiveButtonClickListener)
|
||||
.setNegativeButton(R.string.go_back_button_label, null)
|
||||
.setMessage(R.string.setup_fingerprint_enroll_skip_after_adding_lock_text)
|
||||
.create()
|
@@ -1,283 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.android.internal.widget.LockPatternUtils
|
||||
import com.android.settings.biometrics.BiometricEnrollBase
|
||||
import com.android.settings.biometrics.BiometricUtils
|
||||
import com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like
|
||||
* start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing.
|
||||
*/
|
||||
class AutoCredentialViewModel(
|
||||
application: Application,
|
||||
private val lockPatternUtils: LockPatternUtils,
|
||||
private val challengeGenerator: ChallengeGenerator,
|
||||
private val credentialModel: CredentialModel
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
/**
|
||||
* Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge
|
||||
*/
|
||||
interface GenerateChallengeCallback {
|
||||
/** Generic generateChallenge method for FingerprintManager or FaceManager */
|
||||
fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long)
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic interface class for calling different generateChallenge from FingerprintManager or
|
||||
* FaceManager
|
||||
*/
|
||||
interface ChallengeGenerator {
|
||||
|
||||
/** Callback that will be called later after challenge generated */
|
||||
var callback: GenerateChallengeCallback?
|
||||
|
||||
/** Method for generating challenge from FingerprintManager or FaceManager */
|
||||
fun generateChallenge(userId: Int)
|
||||
}
|
||||
|
||||
/** Used to generate challenge through FingerprintRepository */
|
||||
class FingerprintChallengeGenerator(
|
||||
private val fingerprintRepository: FingerprintRepository
|
||||
) : ChallengeGenerator {
|
||||
|
||||
override var callback: GenerateChallengeCallback? = null
|
||||
|
||||
override fun generateChallenge(userId: Int) {
|
||||
callback?.let {
|
||||
fingerprintRepository.generateChallenge(userId) {
|
||||
sensorId: Int, uid: Int, challenge: Long ->
|
||||
it.onChallengeGenerated(sensorId, uid, challenge)
|
||||
}
|
||||
} ?:run {
|
||||
Log.e(TAG, "generateChallenge, null callback")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FingerprintChallengeGenerator"
|
||||
}
|
||||
}
|
||||
|
||||
private val _generateChallengeFailedFlow = MutableSharedFlow<Boolean>()
|
||||
val generateChallengeFailedFlow: SharedFlow<Boolean>
|
||||
get() = _generateChallengeFailedFlow.asSharedFlow()
|
||||
|
||||
|
||||
// flag if token is generating through checkCredential()'s generateChallenge()
|
||||
private var isGeneratingChallengeDuringCheckingCredential = false
|
||||
|
||||
/** Get bundle which passing back to FingerprintSettings for late generateChallenge() */
|
||||
fun createGeneratingChallengeExtras(): Bundle? {
|
||||
if (!isGeneratingChallengeDuringCheckingCredential
|
||||
|| !credentialModel.isValidToken
|
||||
|| !credentialModel.isValidChallenge
|
||||
) {
|
||||
return null
|
||||
}
|
||||
val bundle = Bundle()
|
||||
bundle.putByteArray(
|
||||
ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
|
||||
credentialModel.token
|
||||
)
|
||||
bundle.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, credentialModel.challenge)
|
||||
return bundle
|
||||
}
|
||||
|
||||
/** Check credential status for biometric enrollment. */
|
||||
fun checkCredential(scope: CoroutineScope): CredentialAction {
|
||||
return if (isValidCredential) {
|
||||
CredentialAction.CREDENTIAL_VALID
|
||||
} else if (isUnspecifiedPassword) {
|
||||
CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK
|
||||
} else if (credentialModel.isValidGkPwHandle) {
|
||||
val gkPwHandle = credentialModel.gkPwHandle
|
||||
credentialModel.clearGkPwHandle()
|
||||
// GkPwHandle is got through caller activity, we shall not revoke it after
|
||||
// generateChallenge(). Let caller activity to make decision.
|
||||
generateChallenge(gkPwHandle, false, scope)
|
||||
isGeneratingChallengeDuringCheckingCredential = true
|
||||
CredentialAction.IS_GENERATING_CHALLENGE
|
||||
} else {
|
||||
CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateChallenge(
|
||||
gkPwHandle: Long,
|
||||
revokeGkPwHandle: Boolean,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
challengeGenerator.callback = object : GenerateChallengeCallback {
|
||||
override fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long) {
|
||||
var illegalStateExceptionCaught = false
|
||||
try {
|
||||
val newToken = requestGatekeeperHat(gkPwHandle, challenge, userId)
|
||||
credentialModel.challenge = challenge
|
||||
credentialModel.token = newToken
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "generateChallenge, IllegalStateException", e)
|
||||
illegalStateExceptionCaught = true
|
||||
} finally {
|
||||
if (revokeGkPwHandle) {
|
||||
lockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle)
|
||||
}
|
||||
Log.d(
|
||||
TAG,
|
||||
"generateChallenge(), model:$credentialModel"
|
||||
+ ", revokeGkPwHandle:$revokeGkPwHandle"
|
||||
)
|
||||
// Check credential again
|
||||
if (!isValidCredential || illegalStateExceptionCaught) {
|
||||
Log.w(TAG, "generateChallenge, invalid Credential or IllegalStateException")
|
||||
scope.launch {
|
||||
_generateChallengeFailedFlow.emit(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
challengeGenerator.generateChallenge(userId)
|
||||
}
|
||||
|
||||
private val isValidCredential: Boolean
|
||||
get() = !isUnspecifiedPassword && credentialModel.isValidToken
|
||||
|
||||
private val isUnspecifiedPassword: Boolean
|
||||
get() = lockPatternUtils.getActivePasswordQuality(userId) == 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 and viewModel is generating challenge
|
||||
*/
|
||||
fun generateChallengeAsCredentialActivityResult(
|
||||
isChooseLock: Boolean,
|
||||
result: ActivityResult,
|
||||
scope: CoroutineScope
|
||||
): Boolean {
|
||||
if ((isChooseLock && result.resultCode == ChooseLockPattern.RESULT_FINISHED) ||
|
||||
(!isChooseLock && result.resultCode == Activity.RESULT_OK)) {
|
||||
result.data?.let {
|
||||
val gkPwHandle = it.getLongExtra(
|
||||
ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE,
|
||||
CredentialModel.INVALID_GK_PW_HANDLE
|
||||
)
|
||||
// Revoke self requested GkPwHandle because it shall only used once inside this
|
||||
// activity lifecycle.
|
||||
generateChallenge(gkPwHandle, true, scope)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val userId: Int
|
||||
get() = credentialModel.userId
|
||||
|
||||
val token: ByteArray?
|
||||
get() = credentialModel.token
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
private fun requestGatekeeperHat(gkPwHandle: Long, challenge: Long, userId: Int): ByteArray? {
|
||||
val response = lockPatternUtils
|
||||
.verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId)
|
||||
if (!response.isMatched) {
|
||||
throw GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT")
|
||||
}
|
||||
return response.gatekeeperHAT
|
||||
}
|
||||
|
||||
/** Create Intent for choosing lock */
|
||||
fun createChooseLockIntent(
|
||||
context: Context, isSuw: Boolean,
|
||||
suwExtras: Bundle
|
||||
): Intent {
|
||||
val 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)
|
||||
if (credentialModel.isValidUserId) {
|
||||
intent.putExtra(Intent.EXTRA_USER_ID, credentialModel.userId)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
/** Create ConfirmLockLauncher */
|
||||
fun createConfirmLockLauncher(
|
||||
activity: Activity,
|
||||
requestCode: Int, title: String
|
||||
): ChooseLockSettingsHelper {
|
||||
val builder = ChooseLockSettingsHelper.Builder(activity)
|
||||
builder.setRequestCode(requestCode)
|
||||
.setTitle(title)
|
||||
.setRequestGatekeeperPasswordHandle(true)
|
||||
.setForegroundOnly(true)
|
||||
.setReturnCredentials(true)
|
||||
if (credentialModel.isValidUserId) {
|
||||
builder.setUserId(credentialModel.userId)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AutoCredentialViewModel"
|
||||
}
|
||||
}
|
||||
|
||||
enum class CredentialAction {
|
||||
|
||||
CREDENTIAL_VALID,
|
||||
|
||||
/** Valid credential, activity does nothing. */
|
||||
IS_GENERATING_CHALLENGE,
|
||||
|
||||
/** This credential looks good, but still need to run generateChallenge(). */
|
||||
FAIL_NEED_TO_CHOOSE_LOCK,
|
||||
|
||||
/** Need activity to run confirm lock */
|
||||
FAIL_NEED_TO_CONFIRM_LOCK
|
||||
}
|
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
|
||||
import com.android.systemui.unfold.updates.FoldProvider;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* ViewModel explaining the fingerprint sensor location for fingerprint enrollment.
|
||||
*/
|
||||
public class DeviceFoldedViewModel extends ViewModel {
|
||||
|
||||
private static final String TAG = "DeviceFoldedViewModel";
|
||||
|
||||
@NonNull private final MutableLiveData<Boolean> mLiveData =
|
||||
new MutableLiveData<>(null);
|
||||
|
||||
private final ScreenSizeFoldProvider mScreenSizeFoldProvider;
|
||||
private final FoldProvider.FoldCallback mIsFoldedCallback = isFolded -> {
|
||||
Log.d(TAG, "onFoldUpdated= " + isFolded);
|
||||
mLiveData.postValue(isFolded);
|
||||
};
|
||||
|
||||
public DeviceFoldedViewModel(@NonNull ScreenSizeFoldProvider screenSizeFoldProvider,
|
||||
@NonNull Executor executor) {
|
||||
super();
|
||||
mScreenSizeFoldProvider = screenSizeFoldProvider;
|
||||
mScreenSizeFoldProvider.registerCallback(mIsFoldedCallback, executor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls this method when activity gets configuration change
|
||||
*/
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
mScreenSizeFoldProvider.onConfigurationChange(newConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns FoldedLiveData
|
||||
*/
|
||||
public LiveData<Boolean> getLiveData() {
|
||||
return mLiveData;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
mScreenSizeFoldProvider.unregisterCallback(mIsFoldedCallback);
|
||||
super.onCleared();
|
||||
}
|
||||
}
|
@@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import static android.hardware.display.DisplayManager.DisplayListener;
|
||||
|
||||
import android.app.Application;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.util.Log;
|
||||
import android.view.DisplayInfo;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* ViewModel explaining the fingerprint sensor location for fingerprint enrollment.
|
||||
*/
|
||||
public class DeviceRotationViewModel extends AndroidViewModel {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "DeviceRotationViewModel";
|
||||
|
||||
private final DisplayManager mDisplayManager;
|
||||
private final boolean mIsReverseDefaultRotation;
|
||||
@NonNull private final DisplayInfo mDisplayInfo = new DisplayInfo();
|
||||
|
||||
/** {@link android.hardware.display.DisplayManager} is a final class, set this member visibility
|
||||
* to 'protected' for testing
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected final DisplayListener mDisplayListener = new DisplayListener() {
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayChanged(int displayId) {
|
||||
final int rotation = getRotation();
|
||||
Log.d(TAG, "onDisplayChanged(" + displayId + "), rotation:" + rotation);
|
||||
mLiveData.postValue(rotation);
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull private final MutableLiveData<Integer> mLiveData = new MutableLiveData<>();
|
||||
|
||||
public DeviceRotationViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
mDisplayManager = application.getSystemService(DisplayManager.class);
|
||||
mDisplayManager.registerDisplayListener(mDisplayListener,
|
||||
application.getMainThreadHandler());
|
||||
mIsReverseDefaultRotation = application.getResources().getBoolean(
|
||||
com.android.internal.R.bool.config_reverseDefaultRotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current rotation.
|
||||
*
|
||||
* {@link android.view.Display} is a final class, set this method visibility to "protected" for
|
||||
* inheriting it in test
|
||||
*/
|
||||
@VisibleForTesting
|
||||
@Surface.Rotation
|
||||
protected int getRotation() {
|
||||
getApplication().getDisplay().getDisplayInfo(mDisplayInfo);
|
||||
if (mIsReverseDefaultRotation) {
|
||||
return (mDisplayInfo.rotation + 1) % 4;
|
||||
} else {
|
||||
return mDisplayInfo.rotation;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns RotationLiveData
|
||||
*/
|
||||
public LiveData<Integer> getLiveData() {
|
||||
final Integer lastRotation = mLiveData.getValue();
|
||||
@Surface.Rotation int newRotation = getRotation();
|
||||
if (lastRotation == null || lastRotation != newRotation) {
|
||||
Log.d(TAG, "getLiveData, update rotation from " + lastRotation + " to " + newRotation);
|
||||
mLiveData.setValue(newRotation);
|
||||
}
|
||||
return mLiveData;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
mDisplayManager.unregisterDisplayListener(mDisplayListener);
|
||||
super.onCleared();
|
||||
}
|
||||
}
|
@@ -1,248 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.app.Application;
|
||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
|
||||
import android.os.VibrationAttributes;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* ViewModel explaining the fingerprint enrolling page
|
||||
*/
|
||||
public class FingerprintEnrollEnrollingViewModel extends AndroidViewModel {
|
||||
|
||||
private static final String TAG = FingerprintEnrollEnrollingViewModel.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final VibrationEffect VIBRATE_EFFECT_ERROR =
|
||||
VibrationEffect.createWaveform(new long[]{0, 5, 55, 60}, -1);
|
||||
private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
|
||||
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
|
||||
|
||||
/**
|
||||
* Enrolling finished
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE = 0;
|
||||
|
||||
/**
|
||||
* Icon touch dialog show
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG = 1;
|
||||
|
||||
/**
|
||||
* Has got latest cancelled event due to user skip
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP = 2;
|
||||
|
||||
/**
|
||||
* Has got latest cancelled event due to back key
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_BACK_PRESSED = 3;
|
||||
|
||||
@IntDef(prefix = { "FINGERPRINT_ENROLL_ENROLLING_ACTION_" }, value = {
|
||||
FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE,
|
||||
FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG,
|
||||
FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface FingerprintEnrollEnrollingAction {}
|
||||
|
||||
private final int mUserId;
|
||||
private boolean mOnBackPressed;
|
||||
private boolean mOnSkipPressed;
|
||||
@NonNull private final FingerprintRepository mFingerprintRepository;
|
||||
private final AccessibilityManager mAccessibilityManager;
|
||||
private final Vibrator mVibrator;
|
||||
|
||||
private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
|
||||
|
||||
public FingerprintEnrollEnrollingViewModel(
|
||||
@NonNull Application application,
|
||||
int userId,
|
||||
@NonNull FingerprintRepository fingerprintRepository
|
||||
) {
|
||||
super(application);
|
||||
mUserId = userId;
|
||||
mFingerprintRepository = fingerprintRepository;
|
||||
mAccessibilityManager = application.getSystemService(AccessibilityManager.class);
|
||||
mVibrator = application.getSystemService(Vibrator.class);
|
||||
}
|
||||
|
||||
public LiveData<Integer> getActionLiveData() {
|
||||
return mActionLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears action live data
|
||||
*/
|
||||
public void clearActionLiveData() {
|
||||
mActionLiveData.setValue(null);
|
||||
}
|
||||
|
||||
public boolean getOnSkipPressed() {
|
||||
return mOnSkipPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicks skip button
|
||||
*/
|
||||
public void setOnSkipPressed() {
|
||||
mOnSkipPressed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrolling is cancelled because user clicks skip
|
||||
*/
|
||||
public void onCancelledDueToOnSkipPressed() {
|
||||
final int action = FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSkipButtonClick, post action " + action);
|
||||
}
|
||||
mOnSkipPressed = false;
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is enrolling finished
|
||||
*/
|
||||
public void onEnrollingDone() {
|
||||
final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_DONE;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEnrollingDone, post action " + action);
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
public boolean getOnBackPressed() {
|
||||
return mOnBackPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Back key is pressed.
|
||||
*/
|
||||
public void setOnBackPressed() {
|
||||
mOnBackPressed = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrollment is cancelled because back key is pressed.
|
||||
*/
|
||||
public void onCancelledDueToOnBackPressed() {
|
||||
final int action = FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_BACK_PRESSED;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCancelledEventReceivedAfterOnBackPressed, post action " + action);
|
||||
}
|
||||
mOnBackPressed = false;
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon touch dialog show
|
||||
*/
|
||||
public void showIconTouchDialog() {
|
||||
final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onIconTouchDialogShow, post action " + action);
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* get enroll stage threshold
|
||||
*/
|
||||
public float getEnrollStageThreshold(int index) {
|
||||
return mFingerprintRepository.getEnrollStageThreshold(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enroll stage count
|
||||
*/
|
||||
public int getEnrollStageCount() {
|
||||
return mFingerprintRepository.getEnrollStageCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests interruption of the accessibility feedback from all accessibility services.
|
||||
*/
|
||||
public void clearTalkback() {
|
||||
mAccessibilityManager.interrupt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the {@link AccessibilityManager} is enabled.
|
||||
*
|
||||
* @return True if this {@link AccessibilityManager} is enabled, false otherwise.
|
||||
*/
|
||||
public boolean isAccessibilityEnabled() {
|
||||
return mAccessibilityManager.isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an {@link AccessibilityEvent}.
|
||||
*/
|
||||
public void sendAccessibilityEvent(CharSequence announcement) {
|
||||
AccessibilityEvent e = AccessibilityEvent.obtain();
|
||||
e.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT);
|
||||
e.setClassName(getClass().getName());
|
||||
e.setPackageName(getApplication().getPackageName());
|
||||
e.getText().add(announcement);
|
||||
mAccessibilityManager.sendAccessibilityEvent(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the touch exploration in the system is enabled.
|
||||
*
|
||||
* @return True if touch exploration is enabled, false otherwise.
|
||||
*/
|
||||
public boolean isTouchExplorationEnabled() {
|
||||
return mAccessibilityManager.isTouchExplorationEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the
|
||||
* caller to specify the vibration is owned by someone else and set a reason for vibration.
|
||||
*/
|
||||
public void vibrateError(String reason) {
|
||||
mVibrator.vibrate(mUserId, getApplication().getOpPackageName(),
|
||||
VIBRATE_EFFECT_ERROR, reason, FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first FingerprintSensorPropertiesInternal from FingerprintManager
|
||||
*/
|
||||
@Nullable
|
||||
public FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() {
|
||||
return mFingerprintRepository.getFirstFingerprintSensorPropertiesInternal();
|
||||
}
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import kotlinx.atomicfu.AtomicBoolean
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
|
||||
class FingerprintEnrollErrorDialogViewModel(
|
||||
application: Application,
|
||||
val isSuw: Boolean
|
||||
): AndroidViewModel(application) {
|
||||
|
||||
private val _isDialogShown: AtomicBoolean = atomic(false)
|
||||
val isDialogShown: Boolean
|
||||
get() = _isDialogShown.value
|
||||
|
||||
private val _newDialogFlow = MutableSharedFlow<Int>()
|
||||
val newDialogFlow: SharedFlow<Int>
|
||||
get() = _newDialogFlow.asSharedFlow()
|
||||
|
||||
private val _triggerRetryFlow = MutableSharedFlow<Any>()
|
||||
val triggerRetryFlow: SharedFlow<Any>
|
||||
get() = _triggerRetryFlow.asSharedFlow()
|
||||
|
||||
private val _setResultFlow = MutableSharedFlow<FingerprintErrorDialogSetResultAction>()
|
||||
val setResultFlow: SharedFlow<FingerprintErrorDialogSetResultAction>
|
||||
get() = _setResultFlow.asSharedFlow()
|
||||
|
||||
suspend fun newDialog(errorMsgId: Int) {
|
||||
_isDialogShown.compareAndSet(expect = false, update = true)
|
||||
_newDialogFlow.emit(errorMsgId)
|
||||
}
|
||||
|
||||
suspend fun triggerRetry() {
|
||||
_isDialogShown.compareAndSet(expect = true, update = false)
|
||||
_triggerRetryFlow.emit(Any())
|
||||
}
|
||||
|
||||
suspend fun setResultAndFinish(action: FingerprintErrorDialogSetResultAction) {
|
||||
_isDialogShown.compareAndSet(expect = true, update = false)
|
||||
_setResultFlow.emit(action)
|
||||
}
|
||||
}
|
||||
|
||||
enum class FingerprintErrorDialogSetResultAction {
|
||||
FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH,
|
||||
FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_TIMEOUT
|
||||
}
|
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2022 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* ViewModel explaining the fingerprint sensor location for fingerprint enrollment.
|
||||
*/
|
||||
public class FingerprintEnrollFindSensorViewModel extends AndroidViewModel {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "FingerprintEnrollFindSensorViewModel";
|
||||
|
||||
/**
|
||||
* User clicks 'Skip' button on this page in Settings
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP = 0;
|
||||
|
||||
/**
|
||||
* User clicks 'Skip' button on this page in SetupWizard flow
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG = 1;
|
||||
|
||||
/**
|
||||
* User clicks 'Start' button on this page
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START = 2;
|
||||
|
||||
@IntDef(prefix = { "FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_" }, value = {
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP,
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG,
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface FingerprintEnrollFindSensorAction {}
|
||||
|
||||
private final AccessibilityManager mAccessibilityManager;
|
||||
|
||||
private final boolean mIsSuw;
|
||||
@NonNull private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
|
||||
|
||||
public FingerprintEnrollFindSensorViewModel(@NonNull Application application, boolean isSuw) {
|
||||
super(application);
|
||||
mAccessibilityManager = application.getSystemService(AccessibilityManager.class);
|
||||
mIsSuw = isSuw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns action live data that user chooses
|
||||
*/
|
||||
public LiveData<Integer> getActionLiveData() {
|
||||
return mActionLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear ActionLiveData to prevent get obsolete data
|
||||
*/
|
||||
public void clearActionLiveData() {
|
||||
mActionLiveData.setValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicks skip button on dialog
|
||||
*/
|
||||
public void onSkipDialogButtonClick() {
|
||||
final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSkipDialogButtonClick, post " + action);
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicks skip button
|
||||
*/
|
||||
public void onSkipButtonClick() {
|
||||
final int action = mIsSuw
|
||||
? FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG
|
||||
: FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSkipButtonClick, post action " + action);
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicks start button
|
||||
*/
|
||||
public void onStartButtonClick() {
|
||||
final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStartButtonClick, post action " + action);
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the info about accessibility is enabled or not
|
||||
*/
|
||||
public boolean isAccessibilityEnabled() {
|
||||
return mAccessibilityManager.isEnabled();
|
||||
}
|
||||
}
|
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import android.annotation.IntDef;
|
||||
import android.app.Application;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Finish ViewModel handles the state of the fingerprint renroll final stage
|
||||
*/
|
||||
public class FingerprintEnrollFinishViewModel extends AndroidViewModel {
|
||||
|
||||
private static final String TAG = FingerprintEnrollFinishViewModel.class.getSimpleName();
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* User clicks "Add" button
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK = 0;
|
||||
|
||||
/**
|
||||
* User clicks "Next" button
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK = 1;
|
||||
|
||||
@IntDef(prefix = { "FINGERPRINT_ENROLL_FINISH_ACTION_" }, value = {
|
||||
FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK,
|
||||
FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface FingerprintEnrollFinishAction {}
|
||||
|
||||
@NonNull private final FingerprintRepository mFingerprintRepository;
|
||||
@NonNull private final EnrollmentRequest mRequest;
|
||||
private final int mUserId;
|
||||
|
||||
private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
|
||||
|
||||
public FingerprintEnrollFinishViewModel(@NonNull Application application, int userId,
|
||||
@NonNull EnrollmentRequest request,
|
||||
@NonNull FingerprintRepository fingerprintRepository) {
|
||||
super(application);
|
||||
mUserId = userId;
|
||||
mRequest = request;
|
||||
mFingerprintRepository = fingerprintRepository;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public EnrollmentRequest getRequest() {
|
||||
return mRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first sensor type is Side fps sensor or not
|
||||
*/
|
||||
public boolean canAssumeSfps() {
|
||||
return mFingerprintRepository.canAssumeSfps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Device allows user to enroll another fingerprint or not.
|
||||
*/
|
||||
public boolean isAnotherFingerprintEnrollable() {
|
||||
return mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId)
|
||||
< mFingerprintRepository.getMaxFingerprints();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear action LiveData
|
||||
*/
|
||||
public void clearActionLiveData() {
|
||||
mActionLiveData.setValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action LiveData
|
||||
*/
|
||||
public LiveData<Integer> getActionLiveData() {
|
||||
return mActionLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle add button Click
|
||||
*/
|
||||
public void onAddButtonClick() {
|
||||
final int action = FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onAddButtonClick post(" + action + ")");
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle next button Click
|
||||
*/
|
||||
public void onNextButtonClick() {
|
||||
final int action = FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK;
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onNextButtonClick post(" + action + ")");
|
||||
}
|
||||
mActionLiveData.postValue(action);
|
||||
}
|
||||
}
|
@@ -1,138 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
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.biometrics2.ui.model.FingerprintEnrollable
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.CONTINUE_ENROLL
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.DONE_AND_FINISH
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.SKIP_OR_CANCEL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Fingerprint intro onboarding page view model implementation */
|
||||
class FingerprintEnrollIntroViewModel(
|
||||
application: Application,
|
||||
private val fingerprintRepository: FingerprintRepository,
|
||||
val request: EnrollmentRequest,
|
||||
private val userId: Int
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
/** User's action flow (like clicking Agree, Skip, or Done) */
|
||||
private val _actionFlow = MutableSharedFlow<FingerprintEnrollIntroAction>()
|
||||
val actionFlow: SharedFlow<FingerprintEnrollIntroAction>
|
||||
get() = _actionFlow.asSharedFlow()
|
||||
|
||||
private fun getEnrollableStatus(): FingerprintEnrollable {
|
||||
val num = fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId)
|
||||
val max =
|
||||
if (request.isSuw && !request.isAfterSuwOrSuwSuggestedAction)
|
||||
fingerprintRepository.getMaxFingerprintsInSuw(
|
||||
getApplication<Application>().resources
|
||||
)
|
||||
else
|
||||
fingerprintRepository.maxFingerprints
|
||||
return if (num >= max)
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
else
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
|
||||
}
|
||||
|
||||
private val hasScrolledToBottomFlow = MutableStateFlow(HAS_SCROLLED_TO_BOTTOM_DEFAULT)
|
||||
private val enrollableStatusFlow = MutableStateFlow(getEnrollableStatus())
|
||||
|
||||
/** Enrollable status and hasScrollToBottom live data */
|
||||
val pageStatusFlow: Flow<FingerprintEnrollIntroStatus> =
|
||||
hasScrolledToBottomFlow.combine(enrollableStatusFlow) {
|
||||
hasScrolledToBottom: Boolean, enrollableStatus: FingerprintEnrollable ->
|
||||
FingerprintEnrollIntroStatus(hasScrolledToBottom, enrollableStatus)
|
||||
}
|
||||
|
||||
fun updateEnrollableStatus(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
enrollableStatusFlow.emit(getEnrollableStatus())
|
||||
}
|
||||
}
|
||||
|
||||
/** The first sensor type is UDFPS sensor or not */
|
||||
val canAssumeUdfps: Boolean
|
||||
get() = fingerprintRepository.canAssumeUdfps()
|
||||
|
||||
/** Update onboarding intro page has scrolled to bottom */
|
||||
fun setHasScrolledToBottom(value: Boolean, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
hasScrolledToBottomFlow.emit(value)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get parental consent required or not during enrollment process */
|
||||
val isParentalConsentRequired: Boolean
|
||||
get() = fingerprintRepository.isParentalConsentRequired(getApplication())
|
||||
|
||||
/** Get fingerprint is disable by admin or not */
|
||||
val isBiometricUnlockDisabledByAdmin: Boolean
|
||||
get() = fingerprintRepository.isDisabledByAdmin(getApplication(), userId)
|
||||
|
||||
/**
|
||||
* User clicks next button
|
||||
*/
|
||||
fun onNextButtonClick(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
when (val status = enrollableStatusFlow.value) {
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX ->
|
||||
_actionFlow.emit(DONE_AND_FINISH)
|
||||
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK ->
|
||||
_actionFlow.emit(CONTINUE_ENROLL)
|
||||
|
||||
else -> Log.w(TAG, "fail to click next, enrolled:$status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** User clicks skip/cancel button */
|
||||
fun onSkipOrCancelButtonClick(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
_actionFlow.emit(SKIP_OR_CANCEL)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FingerprintEnrollIntroViewModel"
|
||||
private const val HAS_SCROLLED_TO_BOTTOM_DEFAULT = false
|
||||
private val ENROLLABLE_STATUS_DEFAULT = FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
enum class FingerprintEnrollIntroAction {
|
||||
/** User clicks 'Done' button on this page */
|
||||
DONE_AND_FINISH,
|
||||
/** User clicks 'Agree' button on this page */
|
||||
CONTINUE_ENROLL,
|
||||
/** User clicks 'Skip' button on this page */
|
||||
SKIP_OR_CANCEL
|
||||
}
|
@@ -1,251 +0,0 @@
|
||||
/*
|
||||
* 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.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_CANCELED;
|
||||
import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
|
||||
|
||||
import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_REMAINING;
|
||||
import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_STEPS;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.hardware.fingerprint.FingerprintManager.EnrollReason;
|
||||
import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintUpdater;
|
||||
import com.android.settings.biometrics.fingerprint.MessageDisplayController;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Progress ViewModel handles the state around biometric enrollment. It manages the state of
|
||||
* enrollment throughout the activity lifecycle so the app can continue after an event like
|
||||
* rotation.
|
||||
*/
|
||||
public class FingerprintEnrollProgressViewModel extends AndroidViewModel {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final String TAG = "FingerprintEnrollProgressViewModel";
|
||||
|
||||
private final MutableLiveData<EnrollmentProgress> mProgressLiveData = new MutableLiveData<>(
|
||||
new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING));
|
||||
private final MutableLiveData<EnrollmentStatusMessage> mHelpMessageLiveData =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<EnrollmentStatusMessage> mErrorMessageLiveData =
|
||||
new MutableLiveData<>();
|
||||
private final MutableLiveData<Object> mCanceledSignalLiveData = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> mAcquireLiveData = new MutableLiveData<>();
|
||||
private final MutableLiveData<Integer> mPointerDownLiveData = new MutableLiveData<>();
|
||||
private final MutableLiveData<Integer> mPointerUpLiveData = new MutableLiveData<>();
|
||||
|
||||
private byte[] mToken = null;
|
||||
private final int mUserId;
|
||||
|
||||
private final FingerprintUpdater mFingerprintUpdater;
|
||||
@Nullable private CancellationSignal mCancellationSignal = null;
|
||||
@NonNull private final LinkedList<CancellationSignal> mCancelingSignalQueue =
|
||||
new LinkedList<>();
|
||||
private final EnrollmentCallback mEnrollmentCallback = new EnrollmentCallback() {
|
||||
|
||||
@Override
|
||||
public void onEnrollmentProgress(int remaining) {
|
||||
final int currentSteps = getSteps();
|
||||
final EnrollmentProgress progress = new EnrollmentProgress(
|
||||
currentSteps == INITIAL_STEPS ? remaining : getSteps(), remaining);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEnrollmentProgress(" + remaining + "), steps: " + currentSteps
|
||||
+ ", post progress as " + progress);
|
||||
}
|
||||
mHelpMessageLiveData.setValue(null);
|
||||
mProgressLiveData.postValue(progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEnrollmentHelp(" + helpMsgId + ", " + helpString + ")");
|
||||
}
|
||||
mHelpMessageLiveData.postValue(new EnrollmentStatusMessage(helpMsgId, helpString));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnrollmentError(int errMsgId, CharSequence errString) {
|
||||
Log.d(TAG, "onEnrollmentError(" + errMsgId + ", " + errString
|
||||
+ "), cancelingQueueSize:" + mCancelingSignalQueue.size());
|
||||
if (FINGERPRINT_ERROR_CANCELED == errMsgId && mCancelingSignalQueue.size() > 0) {
|
||||
mCanceledSignalLiveData.postValue(mCancelingSignalQueue.poll());
|
||||
} else {
|
||||
mErrorMessageLiveData.postValue(new EnrollmentStatusMessage(errMsgId, errString));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAcquired(boolean isAcquiredGood) {
|
||||
mAcquireLiveData.postValue(isAcquiredGood);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUdfpsPointerDown(int sensorId) {
|
||||
mPointerDownLiveData.postValue(sensorId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUdfpsPointerUp(int sensorId) {
|
||||
mPointerUpLiveData.postValue(sensorId);
|
||||
}
|
||||
};
|
||||
|
||||
public FingerprintEnrollProgressViewModel(@NonNull Application application,
|
||||
@NonNull FingerprintUpdater fingerprintUpdater, int userId) {
|
||||
super(application);
|
||||
mFingerprintUpdater = fingerprintUpdater;
|
||||
mUserId = userId;
|
||||
}
|
||||
|
||||
public void setToken(byte[] token) {
|
||||
mToken = token;
|
||||
}
|
||||
|
||||
/**
|
||||
* clear progress
|
||||
*/
|
||||
public void clearProgressLiveData() {
|
||||
mProgressLiveData.setValue(new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING));
|
||||
mHelpMessageLiveData.setValue(null);
|
||||
mErrorMessageLiveData.setValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* clear error message
|
||||
*/
|
||||
public void clearErrorMessageLiveData() {
|
||||
mErrorMessageLiveData.setValue(null);
|
||||
}
|
||||
|
||||
public LiveData<EnrollmentProgress> getProgressLiveData() {
|
||||
return mProgressLiveData;
|
||||
}
|
||||
|
||||
public LiveData<EnrollmentStatusMessage> getHelpMessageLiveData() {
|
||||
return mHelpMessageLiveData;
|
||||
}
|
||||
|
||||
public LiveData<EnrollmentStatusMessage> getErrorMessageLiveData() {
|
||||
return mErrorMessageLiveData;
|
||||
}
|
||||
|
||||
public LiveData<Object> getCanceledSignalLiveData() {
|
||||
return mCanceledSignalLiveData;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getAcquireLiveData() {
|
||||
return mAcquireLiveData;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getPointerDownLiveData() {
|
||||
return mPointerDownLiveData;
|
||||
}
|
||||
|
||||
public LiveData<Integer> getPointerUpLiveData() {
|
||||
return mPointerUpLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts enrollment and return latest isEnrolling() result
|
||||
*/
|
||||
public Object startEnrollment(@EnrollReason int reason) {
|
||||
if (mToken == null) {
|
||||
Log.e(TAG, "Null hardware auth token for enroll");
|
||||
return null;
|
||||
}
|
||||
if (mCancellationSignal != null) {
|
||||
Log.w(TAG, "Enrolling is running, shall not start again");
|
||||
return mCancellationSignal;
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.e(TAG, "startEnrollment(" + reason + ")");
|
||||
}
|
||||
|
||||
// Clear data
|
||||
mProgressLiveData.setValue(new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING));
|
||||
mHelpMessageLiveData.setValue(null);
|
||||
mErrorMessageLiveData.setValue(null);
|
||||
|
||||
mCancellationSignal = new CancellationSignal();
|
||||
|
||||
final Resources res = getApplication().getResources();
|
||||
if (reason == ENROLL_ENROLL
|
||||
&& res.getBoolean(R.bool.enrollment_message_display_controller_flag)) {
|
||||
final EnrollmentCallback callback = new MessageDisplayController(
|
||||
getApplication().getMainThreadHandler(),
|
||||
mEnrollmentCallback,
|
||||
SystemClock.elapsedRealtimeClock(),
|
||||
res.getInteger(R.integer.enrollment_help_minimum_time_display),
|
||||
res.getInteger(R.integer.enrollment_progress_minimum_time_display),
|
||||
res.getBoolean(R.bool.enrollment_progress_priority_over_help),
|
||||
res.getBoolean(R.bool.enrollment_prioritize_acquire_messages),
|
||||
res.getInteger(R.integer.enrollment_collect_time));
|
||||
mFingerprintUpdater.enroll(mToken, mCancellationSignal, mUserId, callback, reason,
|
||||
new Intent());
|
||||
} else {
|
||||
mFingerprintUpdater.enroll(mToken, mCancellationSignal, mUserId, mEnrollmentCallback,
|
||||
reason, new Intent());
|
||||
}
|
||||
return mCancellationSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels enrollment and return latest isEnrolling result
|
||||
*/
|
||||
public boolean cancelEnrollment() {
|
||||
final CancellationSignal cancellationSignal = mCancellationSignal;
|
||||
mCancellationSignal = null;
|
||||
|
||||
if (cancellationSignal == null) {
|
||||
Log.e(TAG, "Fail to cancel enrollment, has cancelled or not start");
|
||||
return false;
|
||||
} else {
|
||||
Log.d(TAG, "enrollment cancelled");
|
||||
}
|
||||
mCancelingSignalQueue.add(cancellationSignal);
|
||||
cancellationSignal.cancel();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isEnrolling() {
|
||||
return (mCancellationSignal != null);
|
||||
}
|
||||
|
||||
private int getSteps() {
|
||||
return mProgressLiveData.getValue().getSteps();
|
||||
}
|
||||
}
|
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.android.settings.biometrics.BiometricEnrollBase
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintEnrollFinish.FINGERPRINT_SUGGESTION_ACTIVITY
|
||||
import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest
|
||||
import kotlinx.atomicfu.AtomicBoolean
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Fingerprint enrollment view model implementation
|
||||
*/
|
||||
class FingerprintEnrollmentViewModel(
|
||||
application: Application,
|
||||
private val fingerprintRepository: FingerprintRepository,
|
||||
val request: EnrollmentRequest
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
val isWaitingActivityResult: AtomicBoolean = atomic(false)
|
||||
|
||||
private val _setResultFlow = MutableSharedFlow<ActivityResult>()
|
||||
val setResultFlow: SharedFlow<ActivityResult>
|
||||
get() = _setResultFlow.asSharedFlow()
|
||||
|
||||
var isNewFingerprintAdded = false
|
||||
set(value) {
|
||||
// Only allow changing this value from false to true
|
||||
if (!field) {
|
||||
field = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get override activity result as current ViewModel status.
|
||||
*
|
||||
* FingerprintEnrollmentActivity supports user enrolls 2nd fingerprint or starts a new flow
|
||||
* through Deferred-SUW, Portal-SUW, or SUW Suggestion. Use a method to get override activity
|
||||
* result instead of putting these if-else on every setResult(), .
|
||||
*/
|
||||
fun getOverrideActivityResult(
|
||||
result: ActivityResult,
|
||||
generatingChallengeExtras: Bundle?
|
||||
): ActivityResult {
|
||||
val newResultCode = if (isNewFingerprintAdded)
|
||||
BiometricEnrollBase.RESULT_FINISHED
|
||||
else if (request.isAfterSuwOrSuwSuggestedAction)
|
||||
BiometricEnrollBase.RESULT_CANCELED
|
||||
else
|
||||
result.resultCode
|
||||
|
||||
var newData = result.data
|
||||
if (newResultCode == BiometricEnrollBase.RESULT_FINISHED
|
||||
&& generatingChallengeExtras != null
|
||||
) {
|
||||
if (newData == null) {
|
||||
newData = Intent()
|
||||
}
|
||||
newData.putExtras(generatingChallengeExtras)
|
||||
}
|
||||
return ActivityResult(newResultCode, newData)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
fun checkFinishActivityDuringOnPause(
|
||||
isActivityFinishing: Boolean,
|
||||
isChangingConfigurations: Boolean,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
if (isChangingConfigurations || isActivityFinishing || request.isSuw
|
||||
|| isWaitingActivityResult.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
_setResultFlow.emit(ActivityResult(BiometricEnrollBase.RESULT_TIMEOUT, null))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Suw fingerprint count extra for statistics
|
||||
*/
|
||||
fun getSuwFingerprintCountExtra(userId: Int) = Bundle().also {
|
||||
it.putInt(
|
||||
SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT,
|
||||
fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the result about fingerprint enrollable
|
||||
*/
|
||||
fun isMaxEnrolledReached(userId: Int): Boolean = with(fingerprintRepository) {
|
||||
maxFingerprints <= getNumOfEnrolledFingerprintsSize(userId)
|
||||
}
|
||||
|
||||
val canAssumeUdfps: Boolean
|
||||
get() = fingerprintRepository.canAssumeUdfps()
|
||||
|
||||
val canAssumeSfps: Boolean
|
||||
get() = fingerprintRepository.canAssumeSfps()
|
||||
|
||||
/**
|
||||
* Update FINGERPRINT_SUGGESTION_ACTIVITY into package manager
|
||||
*/
|
||||
fun updateFingerprintSuggestionEnableState(userId: Int) {
|
||||
// Only show "Add another fingerprint" if the user already enrolled one.
|
||||
// "Add fingerprint" will be shown in the main flow if the user hasn't enrolled any
|
||||
// fingerprints. If the user already added more than one fingerprint, they already know
|
||||
// to add multiple fingerprints so we don't show the suggestion.
|
||||
val state = if (fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId) == 1)
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
else
|
||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||
getApplication<Application>().packageManager.setComponentEnabledSetting(
|
||||
ComponentName(
|
||||
getApplication(),
|
||||
FINGERPRINT_SUGGESTION_ACTIVITY
|
||||
),
|
||||
state,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
Log.d(TAG, "$FINGERPRINT_SUGGESTION_ACTIVITY enabled state: $state")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FingerprintEnrollmentViewModel"
|
||||
}
|
||||
}
|
@@ -1,436 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.widget;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.PathShape;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.os.Build;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.PathParser;
|
||||
import android.util.TypedValue;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settings.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* UDFPS fingerprint drawable that is shown when enrolling
|
||||
*/
|
||||
public class UdfpsEnrollDrawable extends Drawable {
|
||||
private static final String TAG = "UdfpsAnimationEnroll";
|
||||
|
||||
private static final long TARGET_ANIM_DURATION_LONG = 800L;
|
||||
private static final long TARGET_ANIM_DURATION_SHORT = 600L;
|
||||
// 1 + SCALE_MAX is the maximum that the moving target will animate to
|
||||
private static final float SCALE_MAX = 0.25f;
|
||||
private static final float DEFAULT_STROKE_WIDTH = 3f;
|
||||
private static final float SCALE = 0.5f;
|
||||
private static final String SCALE_OVERRIDE =
|
||||
"com.android.systemui.biometrics.UdfpsEnrollHelper.scale";
|
||||
private static final String NEW_COORDS_OVERRIDE =
|
||||
"com.android.systemui.biometrics.UdfpsNewCoords";
|
||||
|
||||
@NonNull
|
||||
private final Drawable mMovingTargetFpIcon;
|
||||
@NonNull
|
||||
private final Paint mSensorOutlinePaint;
|
||||
@NonNull
|
||||
private final Paint mBlueFill;
|
||||
@NonNull
|
||||
private final ShapeDrawable mFingerprintDrawable;
|
||||
private int mAlpha;
|
||||
private boolean mSkipDraw = false;
|
||||
|
||||
@Nullable
|
||||
private RectF mSensorRect;
|
||||
|
||||
// Moving target animator set
|
||||
@Nullable
|
||||
AnimatorSet mTargetAnimatorSet;
|
||||
// Moving target location
|
||||
float mCurrentX;
|
||||
float mCurrentY;
|
||||
// Moving target size
|
||||
float mCurrentScale = 1.f;
|
||||
|
||||
@NonNull
|
||||
private final Animator.AnimatorListener mTargetAnimListener;
|
||||
|
||||
private boolean mShouldShowTipHint = false;
|
||||
private boolean mShouldShowEdgeHint = false;
|
||||
|
||||
private int mEnrollIcon;
|
||||
private int mMovingTargetFill;
|
||||
|
||||
private int mTotalSteps = -1;
|
||||
private int mRemainingSteps = -1;
|
||||
private int mLocationsEnrolled = 0;
|
||||
private int mCenterTouchCount = 0;
|
||||
|
||||
private FingerprintManager mFingerprintManager;
|
||||
|
||||
private boolean mAccessibilityEnabled;
|
||||
private Context mContext;
|
||||
private final List<PointF> mGuidedEnrollmentPoints;
|
||||
|
||||
UdfpsEnrollDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
mFingerprintDrawable = defaultFactory(context);
|
||||
|
||||
loadResources(context, attrs);
|
||||
mSensorOutlinePaint = new Paint(0 /* flags */);
|
||||
mSensorOutlinePaint.setAntiAlias(true);
|
||||
mSensorOutlinePaint.setColor(mMovingTargetFill);
|
||||
mSensorOutlinePaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
mBlueFill = new Paint(0 /* flags */);
|
||||
mBlueFill.setAntiAlias(true);
|
||||
mBlueFill.setColor(mMovingTargetFill);
|
||||
mBlueFill.setStyle(Paint.Style.FILL);
|
||||
|
||||
mMovingTargetFpIcon = context.getResources()
|
||||
.getDrawable(R.drawable.ic_enrollment_fingerprint, null);
|
||||
mMovingTargetFpIcon.setTint(mEnrollIcon);
|
||||
mMovingTargetFpIcon.mutate();
|
||||
|
||||
mFingerprintDrawable.setTint(mEnrollIcon);
|
||||
|
||||
setAlpha(255);
|
||||
mTargetAnimListener = new Animator.AnimatorListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
updateTipHintVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
}
|
||||
};
|
||||
mContext = context;
|
||||
mFingerprintManager = context.getSystemService(FingerprintManager.class);
|
||||
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
|
||||
mAccessibilityEnabled = am.isEnabled();
|
||||
mGuidedEnrollmentPoints = new ArrayList<>();
|
||||
initEnrollPoint(context);
|
||||
}
|
||||
|
||||
/** The [sensorRect] coordinates for the sensor area. */
|
||||
void onSensorRectUpdated(@NonNull RectF sensorRect) {
|
||||
int margin = ((int) sensorRect.height()) / 8;
|
||||
Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin,
|
||||
(int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin);
|
||||
updateFingerprintIconBounds(bounds);
|
||||
mSensorRect = sensorRect;
|
||||
}
|
||||
|
||||
void setShouldSkipDraw(boolean skipDraw) {
|
||||
if (mSkipDraw == skipDraw) {
|
||||
return;
|
||||
}
|
||||
mSkipDraw = skipDraw;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
void updateFingerprintIconBounds(@NonNull Rect bounds) {
|
||||
mFingerprintDrawable.setBounds(bounds);
|
||||
invalidateSelf();
|
||||
mMovingTargetFpIcon.setBounds(bounds);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
void onEnrollmentProgress(final int remaining, final int totalSteps) {
|
||||
if (mTotalSteps == -1) {
|
||||
mTotalSteps = totalSteps;
|
||||
}
|
||||
|
||||
if (remaining != mRemainingSteps) {
|
||||
mLocationsEnrolled++;
|
||||
if (isCenterEnrollmentStage()) {
|
||||
mCenterTouchCount++;
|
||||
}
|
||||
}
|
||||
mRemainingSteps = remaining;
|
||||
|
||||
if (!isCenterEnrollmentStage()) {
|
||||
if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) {
|
||||
mTargetAnimatorSet.end();
|
||||
}
|
||||
|
||||
final PointF point = getNextGuidedEnrollmentPoint();
|
||||
if (mCurrentX != point.x || mCurrentY != point.y) {
|
||||
final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x);
|
||||
x.addUpdateListener(animation -> {
|
||||
mCurrentX = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
|
||||
final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y);
|
||||
y.addUpdateListener(animation -> {
|
||||
mCurrentY = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
});
|
||||
|
||||
final boolean isMovingToCenter = point.x == 0f && point.y == 0f;
|
||||
final long duration = isMovingToCenter
|
||||
? TARGET_ANIM_DURATION_SHORT
|
||||
: TARGET_ANIM_DURATION_LONG;
|
||||
|
||||
final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI);
|
||||
scale.setDuration(duration);
|
||||
scale.addUpdateListener(animation -> {
|
||||
// Grow then shrink
|
||||
mCurrentScale = 1
|
||||
+ SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
});
|
||||
|
||||
mTargetAnimatorSet = new AnimatorSet();
|
||||
|
||||
mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
mTargetAnimatorSet.setDuration(duration);
|
||||
mTargetAnimatorSet.addListener(mTargetAnimListener);
|
||||
mTargetAnimatorSet.playTogether(x, y, scale);
|
||||
mTargetAnimatorSet.start();
|
||||
} else {
|
||||
updateTipHintVisibility();
|
||||
}
|
||||
} else {
|
||||
updateTipHintVisibility();
|
||||
}
|
||||
|
||||
updateEdgeHintVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (mSkipDraw) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw moving target
|
||||
if (!isCenterEnrollmentStage()) {
|
||||
canvas.save();
|
||||
canvas.translate(mCurrentX, mCurrentY);
|
||||
|
||||
if (mSensorRect != null) {
|
||||
canvas.scale(mCurrentScale, mCurrentScale,
|
||||
mSensorRect.centerX(), mSensorRect.centerY());
|
||||
canvas.drawOval(mSensorRect, mBlueFill);
|
||||
}
|
||||
|
||||
mMovingTargetFpIcon.draw(canvas);
|
||||
canvas.restore();
|
||||
} else {
|
||||
if (mSensorRect != null) {
|
||||
canvas.drawOval(mSensorRect, mSensorOutlinePaint);
|
||||
}
|
||||
mFingerprintDrawable.draw(canvas);
|
||||
mFingerprintDrawable.setAlpha(getAlpha());
|
||||
mSensorOutlinePaint.setAlpha(getAlpha());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
mAlpha = alpha;
|
||||
mFingerprintDrawable.setAlpha(alpha);
|
||||
mSensorOutlinePaint.setAlpha(alpha);
|
||||
mBlueFill.setAlpha(alpha);
|
||||
mMovingTargetFpIcon.setAlpha(alpha);
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlpha() {
|
||||
return mAlpha;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void updateTipHintVisibility() {
|
||||
final boolean shouldShow = isTipEnrollmentStage();
|
||||
// With the new update, we will git rid of most of this code, and instead
|
||||
// we will change the fingerprint icon.
|
||||
if (mShouldShowTipHint == shouldShow) {
|
||||
return;
|
||||
}
|
||||
mShouldShowTipHint = shouldShow;
|
||||
}
|
||||
|
||||
private void updateEdgeHintVisibility() {
|
||||
final boolean shouldShow = isEdgeEnrollmentStage();
|
||||
if (mShouldShowEdgeHint == shouldShow) {
|
||||
return;
|
||||
}
|
||||
mShouldShowEdgeHint = shouldShow;
|
||||
}
|
||||
|
||||
private ShapeDrawable defaultFactory(Context context) {
|
||||
String fpPath = context.getResources().getString(R.string.config_udfpsIcon);
|
||||
ShapeDrawable drawable = new ShapeDrawable(
|
||||
new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)
|
||||
);
|
||||
drawable.mutate();
|
||||
drawable.getPaint().setStyle(Paint.Style.STROKE);
|
||||
drawable.getPaint().setStrokeCap(Paint.Cap.ROUND);
|
||||
drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
private void loadResources(Context context, @Nullable AttributeSet attrs) {
|
||||
final TypedArray ta = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
|
||||
R.style.BiometricsEnrollStyle);
|
||||
mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0);
|
||||
mMovingTargetFill = ta.getColor(
|
||||
R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
private boolean isCenterEnrollmentStage() {
|
||||
if (mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return true;
|
||||
}
|
||||
return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
|
||||
}
|
||||
|
||||
private int getStageThresholdSteps(int totalSteps, int stageIndex) {
|
||||
return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
|
||||
}
|
||||
|
||||
private PointF getNextGuidedEnrollmentPoint() {
|
||||
if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
|
||||
return new PointF(0f, 0f);
|
||||
}
|
||||
|
||||
float scale = SCALE;
|
||||
if (Build.IS_ENG || Build.IS_USERDEBUG) {
|
||||
scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
|
||||
SCALE_OVERRIDE, SCALE,
|
||||
UserHandle.USER_CURRENT);
|
||||
}
|
||||
final int index = mLocationsEnrolled - mCenterTouchCount;
|
||||
final PointF originalPoint = mGuidedEnrollmentPoints
|
||||
.get(index % mGuidedEnrollmentPoints.size());
|
||||
return new PointF(originalPoint.x * scale, originalPoint.y * scale);
|
||||
}
|
||||
|
||||
private boolean isGuidedEnrollmentStage() {
|
||||
if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
}
|
||||
final int progressSteps = mTotalSteps - mRemainingSteps;
|
||||
return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
|
||||
&& progressSteps < getStageThresholdSteps(mTotalSteps, 1);
|
||||
}
|
||||
|
||||
private boolean isTipEnrollmentStage() {
|
||||
if (mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
}
|
||||
final int progressSteps = mTotalSteps - mRemainingSteps;
|
||||
return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
|
||||
&& progressSteps < getStageThresholdSteps(mTotalSteps, 2);
|
||||
}
|
||||
|
||||
private boolean isEdgeEnrollmentStage() {
|
||||
if (mTotalSteps == -1 || mRemainingSteps == -1) {
|
||||
return false;
|
||||
}
|
||||
return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
|
||||
}
|
||||
|
||||
private void initEnrollPoint(Context context) {
|
||||
// Number of pixels per mm
|
||||
float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
|
||||
context.getResources().getDisplayMetrics());
|
||||
boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(),
|
||||
NEW_COORDS_OVERRIDE, 0,
|
||||
UserHandle.USER_CURRENT) != 0;
|
||||
if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) {
|
||||
Log.v(TAG, "Using new coordinates");
|
||||
mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, -1.02f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, 1.02f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(0.29f * px, 0.00f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(2.17f * px, -2.35f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(1.07f * px, -3.96f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, -4.31f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, -3.29f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, -1.23f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, 1.23f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, 3.29f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, 4.31f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(1.07f * px, 3.96f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(2.17f * px, 2.35f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(2.58f * px, 0.00f * px));
|
||||
} else {
|
||||
Log.v(TAG, "Using old coordinates");
|
||||
mGuidedEnrollmentPoints.add(new PointF(2.00f * px, 0.00f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(0.87f * px, -2.70f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, -1.31f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, 1.31f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(0.88f * px, 2.70f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(3.94f * px, -1.06f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(2.90f * px, -4.14f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-0.52f * px, -5.95f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-3.33f * px, -3.33f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-3.99f * px, -0.35f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-3.62f * px, 2.54f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(-1.49f * px, 5.57f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(2.29f * px, 4.92f * px));
|
||||
mGuidedEnrollmentPoints.add(new PointF(3.82f * px, 1.78f * px));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,422 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.widget;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Process;
|
||||
import android.os.VibrationAttributes;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.Interpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.settings.R;
|
||||
|
||||
/**
|
||||
* UDFPS enrollment progress bar.
|
||||
*/
|
||||
public class UdfpsEnrollProgressBarDrawable extends Drawable {
|
||||
private static final String TAG = "UdfpsProgressBar";
|
||||
|
||||
private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L;
|
||||
private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L;
|
||||
private static final long FILL_COLOR_ANIMATION_DURATION_MS = 350L;
|
||||
private static final long PROGRESS_ANIMATION_DURATION_MS = 400L;
|
||||
private static final float STROKE_WIDTH_DP = 12f;
|
||||
private static final Interpolator DEACCEL = new DecelerateInterpolator();
|
||||
|
||||
private static final VibrationEffect VIBRATE_EFFECT_ERROR =
|
||||
VibrationEffect.createWaveform(new long[]{0, 5, 55, 60}, -1);
|
||||
private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
|
||||
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
|
||||
|
||||
private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
|
||||
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
|
||||
|
||||
private static final VibrationEffect SUCCESS_VIBRATION_EFFECT =
|
||||
VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
|
||||
|
||||
private final float mStrokeWidthPx;
|
||||
@ColorInt
|
||||
private final int mProgressColor;
|
||||
@ColorInt
|
||||
private final int mHelpColor;
|
||||
@ColorInt
|
||||
private final int mOnFirstBucketFailedColor;
|
||||
@NonNull
|
||||
private final Drawable mCheckmarkDrawable;
|
||||
@NonNull
|
||||
private final Interpolator mCheckmarkInterpolator;
|
||||
@NonNull
|
||||
private final Paint mBackgroundPaint;
|
||||
@VisibleForTesting
|
||||
@NonNull
|
||||
final Paint mFillPaint;
|
||||
@NonNull
|
||||
private final Vibrator mVibrator;
|
||||
@NonNull
|
||||
private final boolean mIsAccessibilityEnabled;
|
||||
@NonNull
|
||||
private final Context mContext;
|
||||
|
||||
private boolean mAfterFirstTouch;
|
||||
|
||||
private int mRemainingSteps = 0;
|
||||
private int mTotalSteps = 0;
|
||||
private float mProgress = 0f;
|
||||
@Nullable
|
||||
private ValueAnimator mProgressAnimator;
|
||||
@NonNull
|
||||
private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener;
|
||||
|
||||
private boolean mShowingHelp = false;
|
||||
@Nullable
|
||||
private ValueAnimator mFillColorAnimator;
|
||||
@NonNull
|
||||
private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener;
|
||||
|
||||
@Nullable
|
||||
private ValueAnimator mBackgroundColorAnimator;
|
||||
@NonNull
|
||||
private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdateListener;
|
||||
|
||||
private boolean mComplete = false;
|
||||
private float mCheckmarkScale = 0f;
|
||||
@Nullable
|
||||
private ValueAnimator mCheckmarkAnimator;
|
||||
@NonNull
|
||||
private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener;
|
||||
|
||||
private int mMovingTargetFill;
|
||||
private int mMovingTargetFillError;
|
||||
private int mEnrollProgress;
|
||||
private int mEnrollProgressHelp;
|
||||
private int mEnrollProgressHelpWithTalkback;
|
||||
|
||||
public UdfpsEnrollProgressBarDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
mContext = context;
|
||||
|
||||
loadResources(context, attrs);
|
||||
float density = context.getResources().getDisplayMetrics().densityDpi;
|
||||
mStrokeWidthPx = STROKE_WIDTH_DP * (density / DisplayMetrics.DENSITY_DEFAULT);
|
||||
mProgressColor = mEnrollProgress;
|
||||
final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
|
||||
mIsAccessibilityEnabled = am.isTouchExplorationEnabled();
|
||||
mOnFirstBucketFailedColor = mMovingTargetFillError;
|
||||
if (!mIsAccessibilityEnabled) {
|
||||
mHelpColor = mEnrollProgressHelp;
|
||||
} else {
|
||||
mHelpColor = mEnrollProgressHelpWithTalkback;
|
||||
}
|
||||
mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark);
|
||||
mCheckmarkDrawable.mutate();
|
||||
mCheckmarkInterpolator = new OvershootInterpolator();
|
||||
|
||||
mBackgroundPaint = new Paint();
|
||||
mBackgroundPaint.setStrokeWidth(mStrokeWidthPx);
|
||||
mBackgroundPaint.setColor(mMovingTargetFill);
|
||||
mBackgroundPaint.setAntiAlias(true);
|
||||
mBackgroundPaint.setStyle(Paint.Style.STROKE);
|
||||
mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
// Progress fill should *not* use the extracted system color.
|
||||
mFillPaint = new Paint();
|
||||
mFillPaint.setStrokeWidth(mStrokeWidthPx);
|
||||
mFillPaint.setColor(mProgressColor);
|
||||
mFillPaint.setAntiAlias(true);
|
||||
mFillPaint.setStyle(Paint.Style.STROKE);
|
||||
mFillPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
mVibrator = mContext.getSystemService(Vibrator.class);
|
||||
|
||||
mProgressUpdateListener = animation -> {
|
||||
mProgress = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
};
|
||||
|
||||
mFillColorUpdateListener = animation -> {
|
||||
mFillPaint.setColor((int) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
};
|
||||
|
||||
mCheckmarkUpdateListener = animation -> {
|
||||
mCheckmarkScale = (float) animation.getAnimatedValue();
|
||||
invalidateSelf();
|
||||
};
|
||||
|
||||
mBackgroundColorUpdateListener = animation -> {
|
||||
mBackgroundPaint.setColor((int) animation.getAnimatedValue());
|
||||
invalidateSelf();
|
||||
};
|
||||
}
|
||||
|
||||
void onEnrollmentProgress(final int remaining, final int totalSteps) {
|
||||
mAfterFirstTouch = true;
|
||||
updateState(remaining, totalSteps, false /* showingHelp */);
|
||||
}
|
||||
|
||||
void onEnrollmentHelp(int remaining, int totalSteps) {
|
||||
updateState(remaining, totalSteps, true /* showingHelp */);
|
||||
}
|
||||
|
||||
void onLastStepAcquired() {
|
||||
updateState(0, mTotalSteps, false /* showingHelp */);
|
||||
}
|
||||
|
||||
private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) {
|
||||
updateProgress(remainingSteps, totalSteps, showingHelp);
|
||||
updateFillColor(showingHelp);
|
||||
}
|
||||
|
||||
private void updateProgress(int remainingSteps, int totalSteps, boolean showingHelp) {
|
||||
if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mShowingHelp) {
|
||||
if (mVibrator != null && mIsAccessibilityEnabled) {
|
||||
mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
|
||||
VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::onEnrollmentHelp",
|
||||
FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
|
||||
}
|
||||
} else {
|
||||
// If the first touch is an error, remainingSteps will be -1 and the callback
|
||||
// doesn't come from onEnrollmentHelp. If we are in the accessibility flow,
|
||||
// we still would like to vibrate.
|
||||
if (mVibrator != null) {
|
||||
if (remainingSteps == -1 && mIsAccessibilityEnabled) {
|
||||
mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
|
||||
VIBRATE_EFFECT_ERROR,
|
||||
getClass().getSimpleName() + "::onFirstTouchError",
|
||||
FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
|
||||
} else if (remainingSteps != -1 && !mIsAccessibilityEnabled) {
|
||||
mVibrator.vibrate(Process.myUid(),
|
||||
mContext.getOpPackageName(),
|
||||
SUCCESS_VIBRATION_EFFECT,
|
||||
getClass().getSimpleName() + "::OnEnrollmentProgress",
|
||||
HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mShowingHelp = showingHelp;
|
||||
mRemainingSteps = remainingSteps;
|
||||
mTotalSteps = totalSteps;
|
||||
|
||||
final int progressSteps = Math.max(0, totalSteps - remainingSteps);
|
||||
|
||||
// If needed, add 1 to progress and total steps to account for initial touch.
|
||||
final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps;
|
||||
final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps;
|
||||
|
||||
final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal);
|
||||
|
||||
if (mProgressAnimator != null && mProgressAnimator.isRunning()) {
|
||||
mProgressAnimator.cancel();
|
||||
}
|
||||
|
||||
mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress);
|
||||
mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS);
|
||||
mProgressAnimator.addUpdateListener(mProgressUpdateListener);
|
||||
mProgressAnimator.start();
|
||||
|
||||
if (remainingSteps == 0) {
|
||||
startCompletionAnimation();
|
||||
} else if (remainingSteps > 0) {
|
||||
rollBackCompletionAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private void animateBackgroundColor() {
|
||||
if (mBackgroundColorAnimator != null && mBackgroundColorAnimator.isRunning()) {
|
||||
mBackgroundColorAnimator.end();
|
||||
}
|
||||
mBackgroundColorAnimator = ValueAnimator.ofArgb(mBackgroundPaint.getColor(),
|
||||
mOnFirstBucketFailedColor);
|
||||
mBackgroundColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
|
||||
mBackgroundColorAnimator.setRepeatCount(1);
|
||||
mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
|
||||
mBackgroundColorAnimator.setInterpolator(DEACCEL);
|
||||
mBackgroundColorAnimator.addUpdateListener(mBackgroundColorUpdateListener);
|
||||
mBackgroundColorAnimator.start();
|
||||
}
|
||||
|
||||
private void updateFillColor(boolean showingHelp) {
|
||||
if (!mAfterFirstTouch && showingHelp) {
|
||||
// If we are on the first touch, animate the background color
|
||||
// instead of the progress color.
|
||||
animateBackgroundColor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) {
|
||||
mFillColorAnimator.end();
|
||||
}
|
||||
|
||||
@ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor;
|
||||
mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor);
|
||||
mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS);
|
||||
mFillColorAnimator.setRepeatCount(1);
|
||||
mFillColorAnimator.setRepeatMode(ValueAnimator.REVERSE);
|
||||
mFillColorAnimator.setInterpolator(DEACCEL);
|
||||
mFillColorAnimator.addUpdateListener(mFillColorUpdateListener);
|
||||
mFillColorAnimator.start();
|
||||
}
|
||||
|
||||
private void startCompletionAnimation() {
|
||||
if (mComplete) {
|
||||
return;
|
||||
}
|
||||
mComplete = true;
|
||||
|
||||
if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
|
||||
mCheckmarkAnimator.cancel();
|
||||
}
|
||||
|
||||
mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f);
|
||||
mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS);
|
||||
mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS);
|
||||
mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator);
|
||||
mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
|
||||
mCheckmarkAnimator.start();
|
||||
}
|
||||
|
||||
private void rollBackCompletionAnimation() {
|
||||
if (!mComplete) {
|
||||
return;
|
||||
}
|
||||
mComplete = false;
|
||||
|
||||
// Adjust duration based on how much of the completion animation has played.
|
||||
final float animatedFraction = mCheckmarkAnimator != null
|
||||
? mCheckmarkAnimator.getAnimatedFraction()
|
||||
: 0f;
|
||||
final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction);
|
||||
|
||||
if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) {
|
||||
mCheckmarkAnimator.cancel();
|
||||
}
|
||||
|
||||
mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f);
|
||||
mCheckmarkAnimator.setDuration(durationMs);
|
||||
mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener);
|
||||
mCheckmarkAnimator.start();
|
||||
}
|
||||
|
||||
private void loadResources(Context context, @Nullable AttributeSet attrs) {
|
||||
final TypedArray ta = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
|
||||
R.style.BiometricsEnrollStyle);
|
||||
mMovingTargetFill = ta.getColor(
|
||||
R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
|
||||
mMovingTargetFillError = ta.getColor(
|
||||
R.styleable.BiometricsEnrollView_biometricsMovingTargetFillError, 0);
|
||||
mEnrollProgress = ta.getColor(
|
||||
R.styleable.BiometricsEnrollView_biometricsEnrollProgress, 0);
|
||||
mEnrollProgressHelp = ta.getColor(
|
||||
R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelp, 0);
|
||||
mEnrollProgressHelpWithTalkback = ta.getColor(
|
||||
R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
canvas.save();
|
||||
|
||||
// Progress starts from the top, instead of the right
|
||||
canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY());
|
||||
|
||||
final float halfPaddingPx = mStrokeWidthPx / 2f;
|
||||
|
||||
if (mProgress < 1f) {
|
||||
// Draw the background color of the progress circle.
|
||||
canvas.drawArc(
|
||||
halfPaddingPx,
|
||||
halfPaddingPx,
|
||||
getBounds().right - halfPaddingPx,
|
||||
getBounds().bottom - halfPaddingPx,
|
||||
0f /* startAngle */,
|
||||
360f /* sweepAngle */,
|
||||
false /* useCenter */,
|
||||
mBackgroundPaint);
|
||||
}
|
||||
|
||||
if (mProgress > 0f) {
|
||||
// Draw the filled portion of the progress circle.
|
||||
canvas.drawArc(
|
||||
halfPaddingPx,
|
||||
halfPaddingPx,
|
||||
getBounds().right - halfPaddingPx,
|
||||
getBounds().bottom - halfPaddingPx,
|
||||
0f /* startAngle */,
|
||||
360f * mProgress /* sweepAngle */,
|
||||
false /* useCenter */,
|
||||
mFillPaint);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
|
||||
if (mCheckmarkScale > 0f) {
|
||||
final float offsetScale = (float) Math.sqrt(2) / 2f;
|
||||
final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale;
|
||||
final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale;
|
||||
final float centerX = getBounds().centerX() + centerXOffset;
|
||||
final float centerY = getBounds().centerY() + centerYOffset;
|
||||
|
||||
final float boundsXOffset =
|
||||
mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale;
|
||||
final float boundsYOffset =
|
||||
mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale;
|
||||
|
||||
final int left = Math.round(centerX - boundsXOffset);
|
||||
final int top = Math.round(centerY - boundsYOffset);
|
||||
final int right = Math.round(centerX + boundsXOffset);
|
||||
final int bottom = Math.round(centerY + boundsYOffset);
|
||||
mCheckmarkDrawable.setBounds(left, top, right, bottom);
|
||||
mCheckmarkDrawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -1,277 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.RotationUtils;
|
||||
import android.view.DisplayInfo;
|
||||
import android.view.Surface;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.systemui.biometrics.UdfpsUtils;
|
||||
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
|
||||
|
||||
/**
|
||||
* View corresponding with udfps_enroll_view.xml
|
||||
*/
|
||||
public class UdfpsEnrollView extends FrameLayout {
|
||||
private static final String TAG = "UdfpsEnrollView";
|
||||
@NonNull
|
||||
private final UdfpsEnrollDrawable mFingerprintDrawable;
|
||||
@NonNull
|
||||
private final UdfpsEnrollProgressBarDrawable mFingerprintProgressDrawable;
|
||||
@NonNull
|
||||
private final Handler mHandler;
|
||||
|
||||
@NonNull
|
||||
private ImageView mFingerprintProgressView;
|
||||
private UdfpsUtils mUdfpsUtils;
|
||||
|
||||
private int mProgressBarRadius;
|
||||
|
||||
private Rect mSensorRect;
|
||||
private UdfpsOverlayParams mOverlayParams;
|
||||
private FingerprintSensorPropertiesInternal mSensorProperties;
|
||||
|
||||
private int mTotalSteps = -1;
|
||||
private int mRemainingSteps = -1;
|
||||
private int mLocationsEnrolled = 0;
|
||||
private int mCenterTouchCount = 0;
|
||||
|
||||
public UdfpsEnrollView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mFingerprintDrawable = new UdfpsEnrollDrawable(mContext, attrs);
|
||||
mFingerprintProgressDrawable = new UdfpsEnrollProgressBarDrawable(context, attrs);
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mUdfpsUtils = new UdfpsUtils();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
ImageView fingerprintView = findViewById(R.id.udfps_enroll_animation_fp_view);
|
||||
fingerprintView.setImageDrawable(mFingerprintDrawable);
|
||||
mFingerprintProgressView = findViewById(R.id.udfps_enroll_animation_fp_progress_view);
|
||||
mFingerprintProgressView.setImageDrawable(mFingerprintProgressDrawable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive enroll progress information from FingerprintEnrollEnrollingUdfpsFragment
|
||||
*/
|
||||
public void onEnrollmentProgress(int remaining, int totalSteps) {
|
||||
if (mTotalSteps == -1) {
|
||||
mTotalSteps = totalSteps;
|
||||
}
|
||||
mRemainingSteps = remaining;
|
||||
mHandler.post(() -> {
|
||||
mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps);
|
||||
mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive enroll help information from FingerprintEnrollEnrollingUdfpsFragment
|
||||
*/
|
||||
public void onEnrollmentHelp() {
|
||||
mHandler.post(
|
||||
() -> mFingerprintProgressDrawable.onEnrollmentHelp(mRemainingSteps, mTotalSteps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive onAcquired from FingerprintEnrollEnrollingUdfpsFragment
|
||||
*/
|
||||
public void onAcquired(boolean isAcquiredGood) {
|
||||
final boolean animateIfLastStepGood =
|
||||
isAcquiredGood && (mRemainingSteps <= 2 && mRemainingSteps >= 0);
|
||||
mHandler.post(() -> {
|
||||
onFingerUp();
|
||||
if (animateIfLastStepGood) mFingerprintProgressDrawable.onLastStepAcquired();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive onPointerDown from FingerprintEnrollEnrollingUdfpsFragment
|
||||
*/
|
||||
public void onPointerDown(int sensorId) {
|
||||
onFingerDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive onPointerUp from FingerprintEnrollEnrollingUdfpsFragment
|
||||
*/
|
||||
public void onPointerUp(int sensorId) {
|
||||
onFingerUp();
|
||||
}
|
||||
|
||||
private final ViewTreeObserver.OnDrawListener mOnDrawListener = this::updateOverlayParams;
|
||||
|
||||
/**
|
||||
* setup SensorProperties
|
||||
*/
|
||||
public void setSensorProperties(FingerprintSensorPropertiesInternal properties) {
|
||||
mSensorProperties = properties;
|
||||
((ViewGroup) getParent()).getViewTreeObserver().addOnDrawListener(mOnDrawListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
final ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
final ViewTreeObserver observer = parent.getViewTreeObserver();
|
||||
if (observer != null) {
|
||||
observer.removeOnDrawListener(mOnDrawListener);
|
||||
}
|
||||
}
|
||||
super.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
private void onSensorRectUpdated() {
|
||||
updateDimensions();
|
||||
|
||||
// Updates sensor rect in relation to the overlay view
|
||||
mSensorRect.set(getPaddingX(), getPaddingY(),
|
||||
(mOverlayParams.getSensorBounds().width() + getPaddingX()),
|
||||
(mOverlayParams.getSensorBounds().height() + getPaddingY()));
|
||||
mFingerprintDrawable.onSensorRectUpdated(new RectF(mSensorRect));
|
||||
}
|
||||
|
||||
private void updateDimensions() {
|
||||
// Original sensorBounds assume portrait mode.
|
||||
final Rect rotatedBounds = new Rect(mOverlayParams.getSensorBounds());
|
||||
int rotation = mOverlayParams.getRotation();
|
||||
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
||||
RotationUtils.rotateBounds(
|
||||
rotatedBounds,
|
||||
mOverlayParams.getNaturalDisplayWidth(),
|
||||
mOverlayParams.getNaturalDisplayHeight(),
|
||||
rotation
|
||||
);
|
||||
}
|
||||
|
||||
RelativeLayout parent = ((RelativeLayout) getParent());
|
||||
if (parent == null) {
|
||||
Log.e(TAG, "Fail to updateDimensions for " + this + ", parent null");
|
||||
return;
|
||||
}
|
||||
final int[] coords = parent.getLocationOnScreen();
|
||||
final int parentLeft = coords[0];
|
||||
final int parentTop = coords[1];
|
||||
final int parentRight = parentLeft + parent.getWidth();
|
||||
final int parentBottom = parentTop + parent.getHeight();
|
||||
|
||||
|
||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(getWidth(),
|
||||
getHeight());
|
||||
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
|
||||
params.rightMargin = parentRight - rotatedBounds.right - getPaddingX();
|
||||
params.topMargin = rotatedBounds.top - parentTop - getPaddingY();
|
||||
} else {
|
||||
if (rotation == Surface.ROTATION_90) {
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
|
||||
params.rightMargin = parentRight - rotatedBounds.right - getPaddingX();
|
||||
params.bottomMargin = parentBottom - rotatedBounds.bottom - getPaddingY();
|
||||
} else {
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
|
||||
params.bottomMargin = parentBottom - rotatedBounds.bottom - getPaddingY();
|
||||
params.leftMargin = rotatedBounds.left - parentLeft - getPaddingX();
|
||||
}
|
||||
}
|
||||
params.height = rotatedBounds.height() + 2 * getPaddingX();
|
||||
params.width = rotatedBounds.width() + 2 * getPaddingY();
|
||||
setLayoutParams(params);
|
||||
}
|
||||
|
||||
private void onFingerDown() {
|
||||
mFingerprintDrawable.setShouldSkipDraw(true);
|
||||
mFingerprintDrawable.invalidateSelf();
|
||||
}
|
||||
|
||||
private void onFingerUp() {
|
||||
mFingerprintDrawable.setShouldSkipDraw(false);
|
||||
mFingerprintDrawable.invalidateSelf();
|
||||
}
|
||||
|
||||
private int getPaddingX() {
|
||||
return mProgressBarRadius;
|
||||
}
|
||||
|
||||
private int getPaddingY() {
|
||||
return mProgressBarRadius;
|
||||
}
|
||||
|
||||
private void updateOverlayParams() {
|
||||
|
||||
if (mSensorProperties == null) {
|
||||
android.util.Log.e(TAG, "There is no sensor info!");
|
||||
return;
|
||||
}
|
||||
|
||||
DisplayInfo displayInfo = new DisplayInfo();
|
||||
if (getDisplay() == null) {
|
||||
android.util.Log.e(TAG, "Can not get display");
|
||||
return;
|
||||
}
|
||||
getDisplay().getDisplayInfo(displayInfo);
|
||||
Rect udfpsBounds = mSensorProperties.getLocation().getRect();
|
||||
float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
|
||||
udfpsBounds.scale(scaleFactor);
|
||||
|
||||
final Rect overlayBounds = new Rect(
|
||||
0, /* left */
|
||||
displayInfo.getNaturalHeight() / 2, /* top */
|
||||
displayInfo.getNaturalWidth(), /* right */
|
||||
displayInfo.getNaturalHeight() /* botom */);
|
||||
|
||||
mOverlayParams = new UdfpsOverlayParams(
|
||||
udfpsBounds,
|
||||
overlayBounds,
|
||||
displayInfo.getNaturalWidth(),
|
||||
displayInfo.getNaturalHeight(),
|
||||
scaleFactor,
|
||||
displayInfo.rotation,
|
||||
mSensorProperties.sensorType);
|
||||
|
||||
post(() -> {
|
||||
mProgressBarRadius =
|
||||
(int) (mOverlayParams.getScaleFactor() * getContext().getResources().getInteger(
|
||||
R.integer.config_udfpsEnrollProgressBar));
|
||||
mSensorRect = new Rect(mOverlayParams.getSensorBounds());
|
||||
|
||||
onSensorRectUpdated();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,6 @@ import com.android.settings.accounts.AccountFeatureProvider
|
||||
import com.android.settings.applications.ApplicationFeatureProvider
|
||||
import com.android.settings.biometrics.face.FaceFeatureProvider
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
|
||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
|
||||
import com.android.settings.bluetooth.BluetoothFeatureProvider
|
||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
|
||||
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
|
||||
@@ -118,11 +117,6 @@ abstract class FeatureFactory {
|
||||
*/
|
||||
abstract val fingerprintFeatureProvider: FingerprintFeatureProvider
|
||||
|
||||
/**
|
||||
* Gets implementation for Biometrics repository provider.
|
||||
*/
|
||||
abstract val biometricsRepositoryProvider: BiometricsRepositoryProvider
|
||||
|
||||
/**
|
||||
* Gets implementation for the WifiTrackerLib.
|
||||
*/
|
||||
|
@@ -31,7 +31,6 @@ import com.android.settings.biometrics.face.FaceFeatureProvider
|
||||
import com.android.settings.biometrics.face.FaceFeatureProviderImpl
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProviderImpl
|
||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProviderImpl
|
||||
import com.android.settings.bluetooth.BluetoothFeatureProvider
|
||||
import com.android.settings.bluetooth.BluetoothFeatureProviderImpl
|
||||
import com.android.settings.connecteddevice.dock.DockUpdaterFeatureProviderImpl
|
||||
@@ -152,8 +151,6 @@ open class FeatureFactoryImpl : FeatureFactory() {
|
||||
FingerprintFeatureProviderImpl()
|
||||
}
|
||||
|
||||
override val biometricsRepositoryProvider by lazy { BiometricsRepositoryProviderImpl() }
|
||||
|
||||
override val wifiTrackerLibProvider: WifiTrackerLibProvider by lazy {
|
||||
WifiTrackerLibProviderImpl()
|
||||
}
|
||||
|
@@ -25,7 +25,6 @@ import com.android.settings.accounts.AccountFeatureProvider;
|
||||
import com.android.settings.applications.ApplicationFeatureProvider;
|
||||
import com.android.settings.biometrics.face.FaceFeatureProvider;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider;
|
||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
|
||||
import com.android.settings.bluetooth.BluetoothFeatureProvider;
|
||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider;
|
||||
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
|
||||
@@ -83,7 +82,6 @@ public class FakeFeatureFactory extends FeatureFactory {
|
||||
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
|
||||
public final FaceFeatureProvider mFaceFeatureProvider;
|
||||
public final FingerprintFeatureProvider mFingerprintFeatureProvider;
|
||||
public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
|
||||
|
||||
public PanelFeatureProvider panelFeatureProvider;
|
||||
public SlicesFeatureProvider slicesFeatureProvider;
|
||||
@@ -139,7 +137,6 @@ public class FakeFeatureFactory extends FeatureFactory {
|
||||
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
|
||||
mFaceFeatureProvider = mock(FaceFeatureProvider.class);
|
||||
mFingerprintFeatureProvider = mock(FingerprintFeatureProvider.class);
|
||||
mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
|
||||
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
|
||||
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
|
||||
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
|
||||
@@ -271,11 +268,6 @@ public class FakeFeatureFactory extends FeatureFactory {
|
||||
return mFingerprintFeatureProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
|
||||
return mBiometricsRepositoryProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
|
||||
return wifiTrackerLibProvider;
|
||||
|
@@ -23,7 +23,6 @@ import com.android.settings.accounts.AccountFeatureProvider
|
||||
import com.android.settings.applications.ApplicationFeatureProvider
|
||||
import com.android.settings.biometrics.face.FaceFeatureProvider
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider
|
||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider
|
||||
import com.android.settings.bluetooth.BluetoothFeatureProvider
|
||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider
|
||||
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider
|
||||
@@ -126,8 +125,6 @@ class FakeFeatureFactory : FeatureFactory() {
|
||||
get() = TODO("Not yet implemented")
|
||||
override val fingerprintFeatureProvider: FingerprintFeatureProvider
|
||||
get() = TODO("Not yet implemented")
|
||||
override val biometricsRepositoryProvider: BiometricsRepositoryProvider
|
||||
get() = TODO("Not yet implemented")
|
||||
override val wifiTrackerLibProvider: WifiTrackerLibProvider
|
||||
get() = TODO("Not yet implemented")
|
||||
override val securitySettingsFeatureProvider: SecuritySettingsFeatureProvider
|
||||
|
@@ -1 +0,0 @@
|
||||
include /src/com/android/settings/biometrics/OWNERS
|
@@ -1,203 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.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.utils.FingerprintRepositoryUtils.newFingerprintRepository;
|
||||
import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints;
|
||||
import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupSuwMaxFingerprintsEnrollable;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.hardware.biometrics.SensorProperties;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
|
||||
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
|
||||
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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FingerprintRepositoryTest {
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
|
||||
@Mock private Resources mResources;
|
||||
@Mock private FingerprintManager mFingerprintManager;
|
||||
|
||||
private Context mContext;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mContext = ApplicationProvider.getApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSensorType_forUnknownSensor() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UNKNOWN, 1);
|
||||
assertThat(repository.canAssumeUdfps()).isFalse();
|
||||
assertThat(repository.canAssumeSfps()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSensorType_forRearSensor() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_REAR, 1);
|
||||
assertThat(repository.canAssumeUdfps()).isFalse();
|
||||
assertThat(repository.canAssumeSfps()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSensorType_forUdfpsUltrasonicSensor() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UDFPS_ULTRASONIC, 1);
|
||||
assertThat(repository.canAssumeUdfps()).isTrue();
|
||||
assertThat(repository.canAssumeSfps()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSensorType_forUdfpsOpticalSensor() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UDFPS_OPTICAL, 1);
|
||||
assertThat(repository.canAssumeUdfps()).isTrue();
|
||||
assertThat(repository.canAssumeSfps()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSensorType_forPowerButtonSensor() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_POWER_BUTTON, 1);
|
||||
assertThat(repository.canAssumeUdfps()).isFalse();
|
||||
assertThat(repository.canAssumeSfps()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSensorType_forHomeButtonSensor() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_HOME_BUTTON, 1);
|
||||
assertThat(repository.canAssumeUdfps()).isFalse();
|
||||
assertThat(repository.canAssumeSfps()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMaxFingerprints() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UNKNOWN, 999);
|
||||
assertThat(repository.getMaxFingerprints()).isEqualTo(999);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetNumOfEnrolledFingerprintsSize() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UNKNOWN, 999);
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, 10, 3);
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, 22, 99);
|
||||
|
||||
assertThat(repository.getNumOfEnrolledFingerprintsSize(10)).isEqualTo(3);
|
||||
assertThat(repository.getNumOfEnrolledFingerprintsSize(22)).isEqualTo(99);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMaxFingerprintsInSuw() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UNKNOWN, 999);
|
||||
setupSuwMaxFingerprintsEnrollable(mContext, mResources, 333);
|
||||
assertThat(repository.getMaxFingerprintsInSuw(mResources))
|
||||
.isEqualTo(333);
|
||||
|
||||
setupSuwMaxFingerprintsEnrollable(mContext, mResources, 20);
|
||||
assertThat(repository.getMaxFingerprintsInSuw(mResources)).isEqualTo(20);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFirstFingerprintSensorPropertiesInternal() {
|
||||
final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
|
||||
final FingerprintSensorPropertiesInternal prop = new FingerprintSensorPropertiesInternal(
|
||||
0 /* sensorId */,
|
||||
SensorProperties.STRENGTH_STRONG,
|
||||
5,
|
||||
new ArrayList<>() /* componentInfo */,
|
||||
TYPE_UDFPS_OPTICAL,
|
||||
true /* resetLockoutRequiresHardwareAuthToken */);
|
||||
props.add(prop);
|
||||
doAnswer(invocation -> {
|
||||
final IFingerprintAuthenticatorsRegisteredCallback callback =
|
||||
invocation.getArgument(0);
|
||||
callback.onAllAuthenticatorsRegistered(props);
|
||||
return null;
|
||||
}).when(mFingerprintManager).addAuthenticatorsRegisteredCallback(any());
|
||||
|
||||
final FingerprintRepository repository = new FingerprintRepository(mFingerprintManager);
|
||||
assertThat(repository.getFirstFingerprintSensorPropertiesInternal()).isEqualTo(prop);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEnrollStageCount() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UNKNOWN, 999);
|
||||
|
||||
final int expectedValue = 24;
|
||||
doReturn(expectedValue).when(mFingerprintManager).getEnrollStageCount();
|
||||
|
||||
assertThat(repository.getEnrollStageCount()).isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEnrollStageThreshold() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UNKNOWN, 999);
|
||||
|
||||
final float expectedValue0 = 0.42f;
|
||||
final float expectedValue1 = 0.24f;
|
||||
final float expectedValue2 = 0.33f;
|
||||
final float expectedValue3 = 0.90f;
|
||||
doReturn(expectedValue0).when(mFingerprintManager).getEnrollStageThreshold(0);
|
||||
doReturn(expectedValue1).when(mFingerprintManager).getEnrollStageThreshold(1);
|
||||
doReturn(expectedValue2).when(mFingerprintManager).getEnrollStageThreshold(2);
|
||||
doReturn(expectedValue3).when(mFingerprintManager).getEnrollStageThreshold(3);
|
||||
|
||||
assertThat(repository.getEnrollStageThreshold(2)).isEqualTo(expectedValue2);
|
||||
assertThat(repository.getEnrollStageThreshold(1)).isEqualTo(expectedValue1);
|
||||
assertThat(repository.getEnrollStageThreshold(3)).isEqualTo(expectedValue3);
|
||||
assertThat(repository.getEnrollStageThreshold(0)).isEqualTo(expectedValue0);
|
||||
}
|
||||
}
|
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.os.UserHandle
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settings.biometrics.BiometricEnrollBase
|
||||
import com.android.settings.password.ChooseLockSettingsHelper
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.Arrays
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CredentialModelTest {
|
||||
|
||||
private val clock = SystemClock.elapsedRealtimeClock()
|
||||
|
||||
@Test
|
||||
fun testNullBundle() {
|
||||
val credentialModel = CredentialModel(null, clock)
|
||||
Truth.assertThat(credentialModel.userId).isEqualTo(UserHandle.myUserId())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newCredentialModelIntentExtras(
|
||||
userId: Int, challenge: Long,
|
||||
token: ByteArray?, gkPwHandle: Long
|
||||
): Bundle {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(Intent.EXTRA_USER_ID, userId)
|
||||
bundle.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
|
||||
bundle.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, token)
|
||||
bundle.putLong(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle)
|
||||
return bundle
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newValidTokenCredentialIntentExtras(userId: Int): Bundle {
|
||||
return newCredentialModelIntentExtras(
|
||||
userId, 1L, byteArrayOf(0, 1, 2),
|
||||
CredentialModel.INVALID_GK_PW_HANDLE
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newOnlySensorValidCredentialIntentExtras(userId: Int): Bundle {
|
||||
return newCredentialModelIntentExtras(
|
||||
userId, CredentialModel.INVALID_CHALLENGE, null,
|
||||
CredentialModel.INVALID_GK_PW_HANDLE
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newGkPwHandleCredentialIntentExtras(userId: Int, gkPwHandle: Long): Bundle {
|
||||
return newCredentialModelIntentExtras(
|
||||
userId,
|
||||
CredentialModel.INVALID_CHALLENGE,
|
||||
null,
|
||||
gkPwHandle
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkBundleLongValue(
|
||||
bundle1: Bundle, bundle2: Bundle,
|
||||
key: String
|
||||
) {
|
||||
if (!bundle1.containsKey(key)) {
|
||||
return
|
||||
}
|
||||
val value1 = bundle1.getInt(key)
|
||||
val value2 = bundle2.getInt(key)
|
||||
Truth.assertWithMessage(
|
||||
"bundle not match, key:" + key + ", value1:" + value1 + ", value2:"
|
||||
+ value2
|
||||
).that(value1).isEqualTo(value2)
|
||||
}
|
||||
|
||||
private fun checkBundleIntValue(
|
||||
bundle1: Bundle, bundle2: Bundle,
|
||||
key: String
|
||||
) {
|
||||
if (!bundle1.containsKey(key)) {
|
||||
return
|
||||
}
|
||||
val value1 = bundle1.getLong(key)
|
||||
val value2 = bundle2.getLong(key)
|
||||
Truth.assertWithMessage(
|
||||
"bundle not match, key:" + key + ", value1:" + value1 + ", value2:"
|
||||
+ value2
|
||||
).that(value1).isEqualTo(value2)
|
||||
}
|
||||
|
||||
private fun checkBundleByteArrayValue(
|
||||
bundle1: Bundle, bundle2: Bundle,
|
||||
key: String
|
||||
) {
|
||||
if (!bundle1.containsKey(key)) {
|
||||
return
|
||||
}
|
||||
val value1 = bundle1.getByteArray(key)
|
||||
val value2 = bundle2.getByteArray(key)
|
||||
val errMsg = ("bundle not match, key:" + key + ", value1:" + Arrays.toString(value1)
|
||||
+ ", value2:" + Arrays.toString(value2))
|
||||
if (value1 == null) {
|
||||
Truth.assertWithMessage(errMsg).that(value2).isNull()
|
||||
} else {
|
||||
Truth.assertWithMessage(errMsg).that(value1.size).isEqualTo(
|
||||
value2!!.size
|
||||
)
|
||||
for (i in value1.indices) {
|
||||
Truth.assertWithMessage(errMsg).that(value1[i]).isEqualTo(
|
||||
value2[i]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settings.biometrics.BiometricEnrollActivity
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper
|
||||
import com.google.common.truth.Truth
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class EnrollmentRequestTest {
|
||||
|
||||
private val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
@Test
|
||||
fun testIsSuw() {
|
||||
// Default false
|
||||
Truth.assertThat(EnrollmentRequest(Intent(), context, true).isSuw).isFalse()
|
||||
Truth.assertThat(EnrollmentRequest(Intent(), context, false).isSuw).isFalse()
|
||||
val trueIntent = Intent()
|
||||
trueIntent.putExtra(WizardManagerHelper.EXTRA_IS_SETUP_FLOW, true)
|
||||
Truth.assertThat(EnrollmentRequest(trueIntent, context, true).isSuw).isTrue()
|
||||
Truth.assertThat(EnrollmentRequest(trueIntent, context, false).isSuw).isFalse()
|
||||
val falseIntent = Intent()
|
||||
trueIntent.putExtra(WizardManagerHelper.EXTRA_IS_SETUP_FLOW, false)
|
||||
Truth.assertThat(EnrollmentRequest(falseIntent, context, true).isSuw).isFalse()
|
||||
Truth.assertThat(EnrollmentRequest(falseIntent, context, false).isSuw).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsAfterSuwOrSuwSuggestedAction() {
|
||||
// Default false
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(Intent(), context, true)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(Intent(), context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
val deferredTrueIntent = Intent()
|
||||
deferredTrueIntent.putExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, true)
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(deferredTrueIntent, context, true)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isTrue()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(deferredTrueIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
val deferredFalseIntent = Intent()
|
||||
deferredFalseIntent.putExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, false)
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(deferredFalseIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(deferredFalseIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
val portalTrueIntent = Intent()
|
||||
portalTrueIntent.putExtra(WizardManagerHelper.EXTRA_IS_PORTAL_SETUP, true)
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(portalTrueIntent, context, true)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isTrue()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(portalTrueIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
val portalFalseIntent = Intent()
|
||||
portalFalseIntent.putExtra(WizardManagerHelper.EXTRA_IS_PORTAL_SETUP, false)
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(portalFalseIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(portalFalseIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
val suggestedTrueIntent = Intent()
|
||||
suggestedTrueIntent.putExtra(WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, true)
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(suggestedTrueIntent, context, true)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isTrue()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(suggestedTrueIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
val suggestedFalseIntent = Intent()
|
||||
suggestedFalseIntent.putExtra(WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, false)
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(suggestedFalseIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
Truth.assertThat(
|
||||
EnrollmentRequest(suggestedFalseIntent, context, false)
|
||||
.isAfterSuwOrSuwSuggestedAction
|
||||
).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSuwExtras_inSuw() {
|
||||
val suwIntent = Intent()
|
||||
suwIntent.putExtra(WizardManagerHelper.EXTRA_IS_SETUP_FLOW, true)
|
||||
val setupRequest = EnrollmentRequest(suwIntent, context, true)
|
||||
val bundle = setupRequest.suwExtras
|
||||
Truth.assertThat(bundle).isNotNull()
|
||||
Truth.assertThat(bundle.size()).isAtLeast(1)
|
||||
Truth.assertThat(bundle.getBoolean(WizardManagerHelper.EXTRA_IS_SETUP_FLOW)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetSuwExtras_notInSuw() {
|
||||
val suwIntent = Intent()
|
||||
suwIntent.putExtra(WizardManagerHelper.EXTRA_IS_SETUP_FLOW, true)
|
||||
val setupRequest = EnrollmentRequest(suwIntent, context, false)
|
||||
val bundle = setupRequest.suwExtras
|
||||
Truth.assertThat(bundle).isNotNull()
|
||||
Truth.assertThat(bundle.size()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsSkipIntro() {
|
||||
// Default false
|
||||
Truth.assertThat(EnrollmentRequest(Intent(), context, true).isSkipIntro).isFalse()
|
||||
Truth.assertThat(EnrollmentRequest(Intent(), context, false).isSkipIntro).isFalse()
|
||||
val trueIntent = Intent()
|
||||
trueIntent.putExtra(BiometricEnrollActivity.EXTRA_SKIP_INTRO, true)
|
||||
Truth.assertThat(EnrollmentRequest(trueIntent, context, true).isSkipIntro).isTrue()
|
||||
Truth.assertThat(EnrollmentRequest(trueIntent, context, false).isSkipIntro).isTrue()
|
||||
val falseIntent = Intent()
|
||||
falseIntent.putExtra(BiometricEnrollActivity.EXTRA_SKIP_INTRO, false)
|
||||
Truth.assertThat(EnrollmentRequest(falseIntent, context, false).isSkipIntro).isFalse()
|
||||
Truth.assertThat(EnrollmentRequest(falseIntent, context, false).isSkipIntro).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsSkipFindSensor() {
|
||||
// Default false
|
||||
Truth.assertThat(EnrollmentRequest(Intent(), context, true).isSkipFindSensor)
|
||||
.isFalse()
|
||||
Truth.assertThat(EnrollmentRequest(Intent(), context, false).isSkipFindSensor)
|
||||
.isFalse()
|
||||
val trueIntent = Intent()
|
||||
trueIntent.putExtra(EnrollmentRequest.EXTRA_SKIP_FIND_SENSOR, true)
|
||||
Truth.assertThat(EnrollmentRequest(trueIntent, context, true).isSkipFindSensor).isTrue()
|
||||
Truth.assertThat(EnrollmentRequest(trueIntent, context, false).isSkipFindSensor).isTrue()
|
||||
val falseIntent = Intent()
|
||||
falseIntent.putExtra(EnrollmentRequest.EXTRA_SKIP_FIND_SENSOR, false)
|
||||
Truth.assertThat(EnrollmentRequest(falseIntent, context, false).isSkipFindSensor)
|
||||
.isFalse()
|
||||
Truth.assertThat(EnrollmentRequest(falseIntent, context, false).isSkipFindSensor)
|
||||
.isFalse()
|
||||
}
|
||||
}
|
@@ -1,519 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.admin.DevicePolicyManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.os.UserHandle
|
||||
import androidx.activity.result.ActivityResult
|
||||
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.biometrics.BiometricEnrollBase
|
||||
import com.android.settings.biometrics2.ui.model.CredentialModel
|
||||
import com.android.settings.biometrics2.ui.model.CredentialModelTest.Companion.newGkPwHandleCredentialIntentExtras
|
||||
import com.android.settings.biometrics2.ui.model.CredentialModelTest.Companion.newOnlySensorValidCredentialIntentExtras
|
||||
import com.android.settings.biometrics2.ui.model.CredentialModelTest.Companion.newValidTokenCredentialIntentExtras
|
||||
import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator
|
||||
import com.android.settings.password.ChooseLockPattern
|
||||
import com.android.settings.password.ChooseLockSettingsHelper
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
import org.mockito.junit.MockitoRule
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class AutoCredentialViewModelTest {
|
||||
|
||||
@get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock private lateinit var lockPatternUtils: LockPatternUtils
|
||||
|
||||
private var challengeGenerator: TestChallengeGenerator = TestChallengeGenerator()
|
||||
|
||||
private lateinit var viewModel: AutoCredentialViewModel
|
||||
private fun newAutoCredentialViewModel(bundle: Bundle?): AutoCredentialViewModel {
|
||||
return AutoCredentialViewModel(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
lockPatternUtils,
|
||||
challengeGenerator,
|
||||
CredentialModel(bundle, SystemClock.elapsedRealtimeClock())
|
||||
)
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
challengeGenerator = TestChallengeGenerator()
|
||||
}
|
||||
|
||||
private fun setupGenerateChallenge(userId: Int, newSensorId: Int, newChallenge: Long) {
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
challengeGenerator.userId = userId
|
||||
challengeGenerator.sensorId = newSensorId
|
||||
challengeGenerator.challenge = newChallenge
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_validCredentialCase() = runTest {
|
||||
val userId = 99
|
||||
viewModel = newAutoCredentialViewModel(newValidTokenCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
// Check viewModel behavior
|
||||
assertThat(action).isEqualTo(CredentialAction.CREDENTIAL_VALID)
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
assertThat(viewModel.createGeneratingChallengeExtras()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_needToChooseLock() = runTest {
|
||||
val userId = 100
|
||||
viewModel = newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED
|
||||
)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
// Check viewModel behavior
|
||||
assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK)
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
assertThat(viewModel.createGeneratingChallengeExtras()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_needToConfirmLockForSomething() = runTest {
|
||||
val userId = 101
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
// Check viewModel behavior
|
||||
assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK)
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
assertThat(viewModel.createGeneratingChallengeExtras()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_needToConfirmLockForNumeric() = runTest {
|
||||
val userId = 102
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
|
||||
)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
// Check viewModel behavior
|
||||
assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK)
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
assertThat(viewModel.createGeneratingChallengeExtras()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_needToConfirmLockForAlphabetic() = runTest {
|
||||
val userId = 103
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
|
||||
)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(this)
|
||||
runCurrent()
|
||||
|
||||
// Check viewModel behavior
|
||||
assertThat(action).isEqualTo(CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK)
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
assertThat(viewModel.createGeneratingChallengeExtras()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_generateChallenge() = runTest {
|
||||
val userId = 104
|
||||
val gkPwHandle = 1111L
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
val newSensorId = 10
|
||||
val newChallenge = 20L
|
||||
setupGenerateChallenge(userId, newSensorId, newChallenge)
|
||||
whenever(
|
||||
lockPatternUtils.verifyGatekeeperPasswordHandle(
|
||||
gkPwHandle,
|
||||
newChallenge,
|
||||
userId
|
||||
)
|
||||
)
|
||||
.thenReturn(newGoodCredential(gkPwHandle, byteArrayOf(1)))
|
||||
val hasCalledRemoveGkPwHandle = AtomicBoolean()
|
||||
Mockito.doAnswer {
|
||||
hasCalledRemoveGkPwHandle.set(true)
|
||||
null
|
||||
}.`when`(lockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
// Check viewModel behavior
|
||||
assertThat(action).isEqualTo(CredentialAction.IS_GENERATING_CHALLENGE)
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
|
||||
// Check data inside CredentialModel
|
||||
assertThat(viewModel.token).isNotNull()
|
||||
assertThat(challengeGenerator.callbackRunCount).isEqualTo(1)
|
||||
assertThat(hasCalledRemoveGkPwHandle.get()).isFalse()
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
val generatingChallengeExtras = viewModel.createGeneratingChallengeExtras()
|
||||
assertThat(generatingChallengeExtras).isNotNull()
|
||||
assertThat(generatingChallengeExtras!!.getLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE))
|
||||
.isEqualTo(newChallenge)
|
||||
val tokens =
|
||||
generatingChallengeExtras.getByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
|
||||
assertThat(tokens).isNotNull()
|
||||
assertThat(tokens!!.size).isEqualTo(1)
|
||||
assertThat(tokens[0]).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckCredential_generateChallengeFail() = runTest {
|
||||
backgroundScope.launch {
|
||||
val userId = 104
|
||||
val gkPwHandle = 1111L
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
val newSensorId = 10
|
||||
val newChallenge = 20L
|
||||
setupGenerateChallenge(userId, newSensorId, newChallenge)
|
||||
whenever(
|
||||
lockPatternUtils.verifyGatekeeperPasswordHandle(
|
||||
gkPwHandle,
|
||||
newChallenge,
|
||||
userId
|
||||
)
|
||||
)
|
||||
.thenReturn(newBadCredential(0))
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run credential check
|
||||
val action = viewModel.checkCredential(this)
|
||||
runCurrent()
|
||||
|
||||
assertThat(action).isEqualTo(CredentialAction.IS_GENERATING_CHALLENGE)
|
||||
assertThat(generateFails.size).isEqualTo(1)
|
||||
assertThat(generateFails[0]).isEqualTo(true)
|
||||
assertThat(challengeGenerator.callbackRunCount).isEqualTo(1)
|
||||
|
||||
// Check createGeneratingChallengeExtras()
|
||||
assertThat(viewModel.createGeneratingChallengeExtras()).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetUserId_fromIntent() {
|
||||
val userId = 106
|
||||
viewModel = newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
|
||||
// Get userId
|
||||
assertThat(viewModel.userId).isEqualTo(userId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateChallengeAsCredentialActivityResult_invalidChooseLock() = runTest {
|
||||
backgroundScope.launch {
|
||||
val userId = 107
|
||||
val gkPwHandle = 3333L
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))
|
||||
val intent = Intent()
|
||||
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run generateChallengeAsCredentialActivityResult()
|
||||
val ret = viewModel.generateChallengeAsCredentialActivityResult(
|
||||
true,
|
||||
ActivityResult(ChooseLockPattern.RESULT_FINISHED + 1, intent),
|
||||
backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(ret).isFalse()
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateChallengeAsCredentialActivityResult_invalidConfirmLock() = runTest {
|
||||
backgroundScope.launch {
|
||||
val userId = 107
|
||||
val gkPwHandle = 3333L
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))
|
||||
val intent = Intent()
|
||||
intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run generateChallengeAsCredentialActivityResult()
|
||||
val ret = viewModel.generateChallengeAsCredentialActivityResult(
|
||||
false,
|
||||
ActivityResult(Activity.RESULT_OK + 1, intent),
|
||||
backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(ret).isFalse()
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateChallengeAsCredentialActivityResult_nullDataChooseLock() = runTest {
|
||||
val userId = 108
|
||||
val gkPwHandle = 4444L
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newGkPwHandleCredentialIntentExtras(userId, gkPwHandle))
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run generateChallengeAsCredentialActivityResult()
|
||||
val ret = viewModel.generateChallengeAsCredentialActivityResult(
|
||||
true,
|
||||
ActivityResult(ChooseLockPattern.RESULT_FINISHED, null),
|
||||
backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(ret).isFalse()
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateChallengeAsCredentialActivityResult_nullDataConfirmLock() = runTest {
|
||||
val userId = 109
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run generateChallengeAsCredentialActivityResult()
|
||||
val ret = viewModel.generateChallengeAsCredentialActivityResult(
|
||||
false,
|
||||
ActivityResult(Activity.RESULT_OK, null),
|
||||
backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(ret).isFalse()
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateChallengeAsCredentialActivityResult_validChooseLock() = runTest {
|
||||
val userId = 108
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
val gkPwHandle = 6666L
|
||||
val newSensorId = 50
|
||||
val newChallenge = 60L
|
||||
setupGenerateChallenge(userId, newSensorId, newChallenge)
|
||||
whenever(
|
||||
lockPatternUtils.verifyGatekeeperPasswordHandle(
|
||||
gkPwHandle,
|
||||
newChallenge,
|
||||
userId
|
||||
)
|
||||
)
|
||||
.thenReturn(newGoodCredential(gkPwHandle, byteArrayOf(1)))
|
||||
val hasCalledRemoveGkPwHandle = AtomicBoolean()
|
||||
Mockito.doAnswer {
|
||||
hasCalledRemoveGkPwHandle.set(true)
|
||||
null
|
||||
}.`when`(lockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run generateChallengeAsCredentialActivityResult()
|
||||
val intent =
|
||||
Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle)
|
||||
val ret = viewModel.generateChallengeAsCredentialActivityResult(
|
||||
true,
|
||||
ActivityResult(ChooseLockPattern.RESULT_FINISHED, intent),
|
||||
backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(ret).isTrue()
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
assertThat(viewModel.token).isNotNull()
|
||||
assertThat(challengeGenerator.callbackRunCount).isEqualTo(1)
|
||||
assertThat(hasCalledRemoveGkPwHandle.get()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGenerateChallengeAsCredentialActivityResult_validConfirmLock() = runTest {
|
||||
val userId = 109
|
||||
viewModel =
|
||||
newAutoCredentialViewModel(newOnlySensorValidCredentialIntentExtras(userId))
|
||||
whenever(lockPatternUtils.getActivePasswordQuality(userId)).thenReturn(
|
||||
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
|
||||
)
|
||||
val gkPwHandle = 5555L
|
||||
val newSensorId = 80
|
||||
val newChallenge = 90L
|
||||
setupGenerateChallenge(userId, newSensorId, newChallenge)
|
||||
whenever(
|
||||
lockPatternUtils.verifyGatekeeperPasswordHandle(
|
||||
gkPwHandle,
|
||||
newChallenge,
|
||||
userId
|
||||
)
|
||||
)
|
||||
.thenReturn(newGoodCredential(gkPwHandle, byteArrayOf(1)))
|
||||
val hasCalledRemoveGkPwHandle = AtomicBoolean()
|
||||
Mockito.doAnswer {
|
||||
hasCalledRemoveGkPwHandle.set(true)
|
||||
null
|
||||
}.`when`(lockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle)
|
||||
|
||||
val generateFails = listOfGenerateChallengeFailedFlow()
|
||||
|
||||
// Run generateChallengeAsCredentialActivityResult()
|
||||
val intent =
|
||||
Intent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gkPwHandle)
|
||||
val ret = viewModel.generateChallengeAsCredentialActivityResult(
|
||||
false,
|
||||
ActivityResult(Activity.RESULT_OK, intent),
|
||||
backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(ret).isTrue()
|
||||
assertThat(generateFails.size).isEqualTo(0)
|
||||
assertThat(viewModel.token).isNotNull()
|
||||
assertThat(challengeGenerator.callbackRunCount).isEqualTo(1)
|
||||
assertThat(hasCalledRemoveGkPwHandle.get()).isTrue()
|
||||
}
|
||||
|
||||
private fun TestScope.listOfGenerateChallengeFailedFlow(): List<Boolean> =
|
||||
mutableListOf<Boolean>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.generateChallengeFailedFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
class TestChallengeGenerator : ChallengeGenerator {
|
||||
var sensorId = -1
|
||||
var userId = UserHandle.myUserId()
|
||||
var challenge = CredentialModel.INVALID_CHALLENGE
|
||||
var callbackRunCount = 0
|
||||
|
||||
override var callback: AutoCredentialViewModel.GenerateChallengeCallback? = null
|
||||
|
||||
override fun generateChallenge(userId: Int) {
|
||||
callback?.let {
|
||||
it.onChallengeGenerated(sensorId, this.userId, challenge)
|
||||
++callbackRunCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newGoodCredential(gkPwHandle: Long, hat: ByteArray): VerifyCredentialResponse {
|
||||
return VerifyCredentialResponse.Builder()
|
||||
.setGatekeeperPasswordHandle(gkPwHandle)
|
||||
.setGatekeeperHAT(hat)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun newBadCredential(timeout: Int): VerifyCredentialResponse {
|
||||
return if (timeout > 0) {
|
||||
VerifyCredentialResponse.fromTimeout(timeout)
|
||||
} else {
|
||||
VerifyCredentialResponse.fromError()
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* 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.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.testutils.InstantTaskExecutorRule;
|
||||
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DeviceFoldedViewModelTest {
|
||||
|
||||
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
private DeviceFoldedViewModel mViewModel;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
final Application application = ApplicationProvider.getApplicationContext();
|
||||
mViewModel = new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application),
|
||||
application.getMainExecutor());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLiveData() {
|
||||
final Configuration config1 = new Configuration();
|
||||
config1.smallestScreenWidthDp = 601;
|
||||
mViewModel.onConfigurationChanged(config1);
|
||||
assertThat(mViewModel.getLiveData().getValue()).isFalse();
|
||||
|
||||
final Configuration config2 = new Configuration();
|
||||
config2.smallestScreenWidthDp = 599;
|
||||
mViewModel.onConfigurationChanged(config2);
|
||||
assertThat(mViewModel.getLiveData().getValue()).isTrue();
|
||||
}
|
||||
}
|
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
* 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.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.Application;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
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.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class DeviceRotationViewModelTest {
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
private TestDeviceRotationViewModel mViewModel;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
TestDeviceRotationViewModel.sTestRotation = 3;
|
||||
mViewModel = new TestDeviceRotationViewModel(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultLiveDataNotNull() {
|
||||
assertThat(mViewModel.getLiveData().getValue()).isEqualTo(mViewModel.sTestRotation);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnDisplayChange() {
|
||||
mViewModel.sTestRotation = 3;
|
||||
mViewModel.triggerOnDisplayChanged();
|
||||
assertThat(mViewModel.getLiveData().getValue()).isEqualTo(mViewModel.sTestRotation);
|
||||
}
|
||||
|
||||
public static class TestDeviceRotationViewModel extends DeviceRotationViewModel {
|
||||
|
||||
@Surface.Rotation static int sTestRotation = 0;
|
||||
|
||||
public TestDeviceRotationViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
void triggerOnDisplayChanged() {
|
||||
mDisplayListener.onDisplayChanged(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getRotation() {
|
||||
return sTestRotation;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,173 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
|
||||
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_BACK_PRESSED;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel.FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP;
|
||||
import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
|
||||
import android.app.Application;
|
||||
import android.hardware.biometrics.SensorProperties;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
|
||||
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
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.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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FingerprintEnrollEnrollingViewModelTest {
|
||||
|
||||
private static final int TEST_USER_ID = 33;
|
||||
|
||||
@Rule
|
||||
public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
@Rule
|
||||
public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Mock
|
||||
private FingerprintManager mFingerprintManager;
|
||||
|
||||
private Application mApplication;
|
||||
private FingerprintEnrollEnrollingViewModel mViewModel;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mApplication = ApplicationProvider.getApplicationContext();
|
||||
mViewModel = new FingerprintEnrollEnrollingViewModel(
|
||||
mApplication,
|
||||
TEST_USER_ID,
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIconTouchDialog() {
|
||||
final LiveData<Integer> actionLiveData = mViewModel.getActionLiveData();
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(null);
|
||||
|
||||
mViewModel.showIconTouchDialog();
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_ICON_TOUCH_DIALOG);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tesBackPressedScenario() {
|
||||
final LiveData<Integer> actionLiveData = mViewModel.getActionLiveData();
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(null);
|
||||
assertThat(mViewModel.getOnBackPressed()).isEqualTo(false);
|
||||
|
||||
mViewModel.setOnBackPressed();
|
||||
assertThat(mViewModel.getOnBackPressed()).isEqualTo(true);
|
||||
|
||||
mViewModel.onCancelledDueToOnBackPressed();
|
||||
assertThat(mViewModel.getOnBackPressed()).isEqualTo(false);
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_BACK_PRESSED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSkipPressedScenario() {
|
||||
final LiveData<Integer> actionLiveData = mViewModel.getActionLiveData();
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(null);
|
||||
assertThat(mViewModel.getOnSkipPressed()).isEqualTo(false);
|
||||
|
||||
mViewModel.setOnSkipPressed();
|
||||
assertThat(mViewModel.getOnSkipPressed()).isEqualTo(true);
|
||||
|
||||
mViewModel.onCancelledDueToOnSkipPressed();
|
||||
assertThat(mViewModel.getOnSkipPressed()).isEqualTo(false);
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_ENROLLING_CANCELED_BECAUSE_USER_SKIP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetFirstFingerprintSensorPropertiesInternal() {
|
||||
final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
|
||||
final FingerprintSensorPropertiesInternal prop = new FingerprintSensorPropertiesInternal(
|
||||
0 /* sensorId */,
|
||||
SensorProperties.STRENGTH_STRONG,
|
||||
5,
|
||||
new ArrayList<>() /* componentInfo */,
|
||||
TYPE_UDFPS_OPTICAL,
|
||||
true /* resetLockoutRequiresHardwareAuthToken */);
|
||||
props.add(prop);
|
||||
doAnswer(invocation -> {
|
||||
final IFingerprintAuthenticatorsRegisteredCallback callback =
|
||||
invocation.getArgument(0);
|
||||
callback.onAllAuthenticatorsRegistered(props);
|
||||
return null;
|
||||
}).when(mFingerprintManager).addAuthenticatorsRegisteredCallback(any());
|
||||
|
||||
mViewModel = new FingerprintEnrollEnrollingViewModel(
|
||||
mApplication,
|
||||
TEST_USER_ID,
|
||||
new FingerprintRepository(mFingerprintManager)
|
||||
);
|
||||
|
||||
assertThat(mViewModel.getFirstFingerprintSensorPropertiesInternal()).isEqualTo(prop);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEnrollStageCount() {
|
||||
final int expectedValue = 24;
|
||||
doReturn(expectedValue).when(mFingerprintManager).getEnrollStageCount();
|
||||
|
||||
assertThat(mViewModel.getEnrollStageCount()).isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEnrollStageThreshold() {
|
||||
final float expectedValue0 = 0.42f;
|
||||
final float expectedValue1 = 0.24f;
|
||||
final float expectedValue2 = 0.33f;
|
||||
final float expectedValue3 = 0.90f;
|
||||
|
||||
doReturn(expectedValue0).when(mFingerprintManager).getEnrollStageThreshold(0);
|
||||
doReturn(expectedValue1).when(mFingerprintManager).getEnrollStageThreshold(1);
|
||||
doReturn(expectedValue2).when(mFingerprintManager).getEnrollStageThreshold(2);
|
||||
doReturn(expectedValue3).when(mFingerprintManager).getEnrollStageThreshold(3);
|
||||
|
||||
assertThat(mViewModel.getEnrollStageThreshold(2)).isEqualTo(expectedValue2);
|
||||
assertThat(mViewModel.getEnrollStageThreshold(1)).isEqualTo(expectedValue1);
|
||||
assertThat(mViewModel.getEnrollStageThreshold(3)).isEqualTo(expectedValue3);
|
||||
assertThat(mViewModel.getEnrollStageThreshold(0)).isEqualTo(expectedValue0);
|
||||
}
|
||||
}
|
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintErrorDialogSetResultAction.FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FingerprintEnrollErrorDialogViewModelTest {
|
||||
|
||||
private val application = ApplicationProvider.getApplicationContext<Application>()
|
||||
private var viewModel: FingerprintEnrollErrorDialogViewModel =
|
||||
FingerprintEnrollErrorDialogViewModel(application, false)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
// Make sure viewModel is new for each test
|
||||
viewModel = FingerprintEnrollErrorDialogViewModel(application, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsDialogNotShownDefaultFalse() {
|
||||
assertThat(viewModel.isDialogShown).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsSuw() {
|
||||
assertThat(FingerprintEnrollErrorDialogViewModel(application, false).isSuw).isFalse()
|
||||
assertThat(FingerprintEnrollErrorDialogViewModel(application, true).isSuw).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNewDialog() = runTest {
|
||||
val newDialogs: List<Int> = mutableListOf<Int>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.newDialogFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
runCurrent()
|
||||
|
||||
// Default values
|
||||
assertThat(viewModel.isDialogShown).isFalse()
|
||||
assertThat(newDialogs.size).isEqualTo(0)
|
||||
|
||||
val testErrorMsgId = 3456
|
||||
viewModel.newDialog(testErrorMsgId)
|
||||
runCurrent()
|
||||
|
||||
// verify after emit
|
||||
assertThat(viewModel.isDialogShown).isTrue()
|
||||
assertThat(newDialogs.size).isEqualTo(1)
|
||||
assertThat(newDialogs[0]).isEqualTo(testErrorMsgId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTriggerRetry() = runTest {
|
||||
val triggerRetries: List<Any> = mutableListOf<Any>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.triggerRetryFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
runCurrent()
|
||||
|
||||
// Default values
|
||||
assertThat(triggerRetries.size).isEqualTo(0)
|
||||
|
||||
viewModel.triggerRetry()
|
||||
runCurrent()
|
||||
|
||||
// verify after emit
|
||||
assertThat(triggerRetries.size).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetResultFinish() = runTest {
|
||||
val setResults: List<FingerprintErrorDialogSetResultAction> =
|
||||
mutableListOf<FingerprintErrorDialogSetResultAction>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.setResultFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
runCurrent()
|
||||
|
||||
// Default values
|
||||
assertThat(setResults.size).isEqualTo(0)
|
||||
|
||||
viewModel.setResultAndFinish(FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH)
|
||||
runCurrent()
|
||||
|
||||
// verify after emit
|
||||
assertThat(setResults.size).isEqualTo(1)
|
||||
assertThat(setResults[0]).isEqualTo(FINGERPRINT_ERROR_DIALOG_ACTION_SET_RESULT_FINISH)
|
||||
}
|
||||
}
|
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.testutils.InstantTaskExecutorRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FingerprintEnrollFindSensorViewModelTest {
|
||||
|
||||
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
private Application mApplication;
|
||||
private FingerprintEnrollFindSensorViewModel mViewModel;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mApplication = ApplicationProvider.getApplicationContext();
|
||||
mViewModel = new FingerprintEnrollFindSensorViewModel(mApplication, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClickSkipButtonNotInSuw() {
|
||||
mViewModel = new FingerprintEnrollFindSensorViewModel(mApplication, false);
|
||||
mViewModel.onSkipButtonClick();
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClickSkipButtonInSuw() {
|
||||
mViewModel = new FingerprintEnrollFindSensorViewModel(mApplication, true);
|
||||
mViewModel.onSkipButtonClick();
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClickSkipDialogButton() {
|
||||
mViewModel.onSkipDialogButtonClick();
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClickStartDialogButton() {
|
||||
mViewModel.onStartButtonClick();
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearActionLiveData() {
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isNull();
|
||||
|
||||
mViewModel.onStartButtonClick();
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isNotNull();
|
||||
|
||||
mViewModel.clearActionLiveData();
|
||||
assertThat(mViewModel.getActionLiveData().getValue()).isNull();
|
||||
}
|
||||
}
|
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import 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 com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel.FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK;
|
||||
import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository;
|
||||
import static com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
|
||||
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 FingerprintEnrollFinishViewModelTest {
|
||||
|
||||
private static final int USER_ID = 334;
|
||||
private static final int MAX_ENROLLABLE = 5;
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Mock private FingerprintManager mFingerprintManager;
|
||||
|
||||
private Application mApplication;
|
||||
private EnrollmentRequest mRequest;
|
||||
private FingerprintEnrollFinishViewModel mViewModel;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mApplication = ApplicationProvider.getApplicationContext();
|
||||
mRequest = new EnrollmentRequest(new Intent(), mApplication, true);
|
||||
mViewModel = new FingerprintEnrollFinishViewModel(mApplication, USER_ID, mRequest,
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, MAX_ENROLLABLE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCanAssumeSfps() {
|
||||
mViewModel = new FingerprintEnrollFinishViewModel(mApplication, USER_ID, mRequest,
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, MAX_ENROLLABLE));
|
||||
assertThat(mViewModel.canAssumeSfps()).isFalse();
|
||||
|
||||
mViewModel = new FingerprintEnrollFinishViewModel(mApplication, USER_ID, mRequest,
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_REAR, MAX_ENROLLABLE));
|
||||
assertThat(mViewModel.canAssumeSfps()).isFalse();
|
||||
|
||||
mViewModel = new FingerprintEnrollFinishViewModel(mApplication, USER_ID, mRequest,
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_POWER_BUTTON, MAX_ENROLLABLE));
|
||||
assertThat(mViewModel.canAssumeSfps()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAnotherFingerprintEnrollable() {
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, USER_ID, MAX_ENROLLABLE);
|
||||
assertThat(mViewModel.isAnotherFingerprintEnrollable()).isFalse();
|
||||
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, USER_ID, MAX_ENROLLABLE - 1);
|
||||
assertThat(mViewModel.isAnotherFingerprintEnrollable()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetRequest() {
|
||||
assertThat(mViewModel.getRequest()).isEqualTo(mRequest);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnAddButtonClick() {
|
||||
final LiveData<Integer> actionLiveData = mViewModel.getActionLiveData();
|
||||
|
||||
// Test init value
|
||||
assertThat(actionLiveData.getValue()).isNull();
|
||||
|
||||
// Test onAddButtonClick()
|
||||
mViewModel.onAddButtonClick();
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_FINISH_ACTION_ADD_BUTTON_CLICK);
|
||||
|
||||
// Clear
|
||||
mViewModel.clearActionLiveData();
|
||||
assertThat(actionLiveData.getValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnNextButtonClick() {
|
||||
final LiveData<Integer> actionLiveData = mViewModel.getActionLiveData();
|
||||
|
||||
// Test init value
|
||||
assertThat(actionLiveData.getValue()).isNull();
|
||||
|
||||
// Test onNextButtonClick()
|
||||
mViewModel.onNextButtonClick();
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(
|
||||
FINGERPRINT_ENROLL_FINISH_ACTION_NEXT_BUTTON_CLICK);
|
||||
|
||||
// Clear
|
||||
mViewModel.clearActionLiveData();
|
||||
assertThat(actionLiveData.getValue()).isNull();
|
||||
}
|
||||
}
|
@@ -1,357 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Resources
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
|
||||
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.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.CONTINUE_ENROLL
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.DONE_AND_FINISH
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.SKIP_OR_CANCEL
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newAllFalseRequest
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwDeferredRequest
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwPortalRequest
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwRequest
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwSuggestedActionFlowRequest
|
||||
import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository
|
||||
import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints
|
||||
import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupSuwMaxFingerprintsEnrollable
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.junit.MockitoJUnit
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class FingerprintEnrollIntroViewModelTest {
|
||||
|
||||
@get:Rule val mockito = MockitoJUnit.rule()
|
||||
|
||||
@Mock private lateinit var resources: Resources
|
||||
@Mock private lateinit var fingerprintManager: FingerprintManager
|
||||
|
||||
private var application: Application = ApplicationProvider.getApplicationContext()
|
||||
|
||||
private fun newFingerprintEnrollIntroViewModel(
|
||||
fingerprintRepository: FingerprintRepository,
|
||||
enrollmentRequest: EnrollmentRequest
|
||||
) = FingerprintEnrollIntroViewModel(
|
||||
application,
|
||||
fingerprintRepository,
|
||||
enrollmentRequest,
|
||||
TEST_USER_ID
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
application = ApplicationProvider.getApplicationContext()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPageStatusFlowDefaultAndUpdate() = runTest {
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 1),
|
||||
newAllFalseRequest(application)
|
||||
)
|
||||
|
||||
val statusList = listOfPageStatusFlow(viewModel)
|
||||
|
||||
runCurrent()
|
||||
|
||||
// assert default values
|
||||
assertThat(statusList.size).isEqualTo(1)
|
||||
assertThat(statusList[0].hasScrollToBottom()).isFalse()
|
||||
assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_OK)
|
||||
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 1)
|
||||
viewModel.updateEnrollableStatus(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
// assert new updated value
|
||||
assertThat(statusList.size).isEqualTo(2)
|
||||
assertThat(statusList[1].hasScrollToBottom()).isFalse()
|
||||
assertThat(statusList[1].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
|
||||
}
|
||||
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuw() = runTest {
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 0)
|
||||
setupSuwMaxFingerprintsEnrollable(application, resources, 1)
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(application)
|
||||
)
|
||||
|
||||
val statusList = listOfPageStatusFlow(viewModel)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(statusList.size).isEqualTo(1)
|
||||
assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_OK)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuw() = runTest {
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 1)
|
||||
setupSuwMaxFingerprintsEnrollable(application, resources, 1)
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(application)
|
||||
)
|
||||
|
||||
val statusList = listOfPageStatusFlow(viewModel)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(statusList.size).isEqualTo(1)
|
||||
assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isNotSuw() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuwPortal() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(application))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(
|
||||
newIsSuwSuggestedActionFlowRequest(application)
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestScope.testOnStartToUpdateEnrollableStatusOk(request: EnrollmentRequest) {
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 0)
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
request
|
||||
)
|
||||
|
||||
val statusList = listOfPageStatusFlow(viewModel)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(statusList.size).isEqualTo(1)
|
||||
assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_OK)
|
||||
}
|
||||
|
||||
private fun TestScope.testOnStartToUpdateEnrollableStatusReachMax(request: EnrollmentRequest) {
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 5)
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
request
|
||||
)
|
||||
|
||||
val statusList = listOfPageStatusFlow(viewModel)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(statusList.size).isEqualTo(1)
|
||||
assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsParentalConsentRequired() {
|
||||
// We shall not mock FingerprintRepository, but
|
||||
// FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
|
||||
// mock static method
|
||||
val fingerprintRepository = Mockito.mock(
|
||||
FingerprintRepository::class.java
|
||||
)
|
||||
val viewModel = FingerprintEnrollIntroViewModel(
|
||||
application,
|
||||
fingerprintRepository,
|
||||
newAllFalseRequest(application),
|
||||
TEST_USER_ID
|
||||
)
|
||||
Mockito.`when`(
|
||||
fingerprintRepository.isParentalConsentRequired(application)
|
||||
).thenReturn(true)
|
||||
assertThat(viewModel.isParentalConsentRequired).isEqualTo(true)
|
||||
Mockito.`when`(
|
||||
fingerprintRepository.isParentalConsentRequired(application)
|
||||
).thenReturn(false)
|
||||
assertThat(viewModel.isParentalConsentRequired).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsBiometricUnlockDisabledByAdmin() {
|
||||
// We shall not mock FingerprintRepository, but
|
||||
// FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
|
||||
// static method
|
||||
val fingerprintRepository = Mockito.mock(FingerprintRepository::class.java)
|
||||
val viewModel = FingerprintEnrollIntroViewModel(
|
||||
application,
|
||||
fingerprintRepository,
|
||||
newAllFalseRequest(application),
|
||||
TEST_USER_ID
|
||||
)
|
||||
Mockito.`when`(
|
||||
fingerprintRepository.isDisabledByAdmin(application, TEST_USER_ID)
|
||||
).thenReturn(true)
|
||||
assertThat(viewModel.isBiometricUnlockDisabledByAdmin).isEqualTo(true)
|
||||
Mockito.`when`(
|
||||
fingerprintRepository.isDisabledByAdmin(application, TEST_USER_ID)
|
||||
).thenReturn(false)
|
||||
assertThat(viewModel.isBiometricUnlockDisabledByAdmin).isEqualTo(false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetHasScrolledToBottom() = runTest {
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(application)
|
||||
)
|
||||
|
||||
val pageStatusList = listOfPageStatusFlow(viewModel)
|
||||
|
||||
viewModel.setHasScrolledToBottom(true, backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
assertThat(pageStatusList[pageStatusList.size-1].hasScrollToBottom()).isEqualTo(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNextButtonClick_enrollNext() = runTest {
|
||||
// Set latest status to FINGERPRINT_ENROLLABLE_OK
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 0)
|
||||
setupSuwMaxFingerprintsEnrollable(application, resources, 1)
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(application)
|
||||
)
|
||||
|
||||
val actions = listOfActionFlow(viewModel)
|
||||
|
||||
// Perform click on `next`
|
||||
viewModel.onNextButtonClick(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
assertThat(actions.size).isEqualTo(1)
|
||||
assertThat(actions[0]).isEqualTo(CONTINUE_ENROLL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnNextButtonClick_doneAndFinish() = runTest {
|
||||
// Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, TEST_USER_ID, 1)
|
||||
setupSuwMaxFingerprintsEnrollable(application, resources, 1)
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(application)
|
||||
)
|
||||
|
||||
val statusList = listOfPageStatusFlow(viewModel)
|
||||
val actionList = listOfActionFlow(viewModel)
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(statusList.size).isEqualTo(1)
|
||||
assertThat(statusList[0].enrollableStatus).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX)
|
||||
|
||||
val actions = listOfActionFlow(viewModel)
|
||||
|
||||
// Perform click on `next`
|
||||
viewModel.onNextButtonClick(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
assertThat(actionList.size).isEqualTo(1)
|
||||
assertThat(actionList[0]).isEqualTo(DONE_AND_FINISH)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnSkipOrCancelButtonClick() = runTest {
|
||||
val viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(fingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(application)
|
||||
)
|
||||
|
||||
val actions = listOfActionFlow(viewModel)
|
||||
|
||||
viewModel.onSkipOrCancelButtonClick(backgroundScope)
|
||||
runCurrent()
|
||||
|
||||
assertThat(actions.size).isEqualTo(1)
|
||||
assertThat(actions[0]).isEqualTo(SKIP_OR_CANCEL)
|
||||
}
|
||||
|
||||
private fun TestScope.listOfActionFlow(
|
||||
viewModel: FingerprintEnrollIntroViewModel
|
||||
): List<FingerprintEnrollIntroAction> =
|
||||
mutableListOf<FingerprintEnrollIntroAction>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.actionFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.listOfPageStatusFlow(
|
||||
viewModel: FingerprintEnrollIntroViewModel
|
||||
): List<FingerprintEnrollIntroStatus> =
|
||||
mutableListOf<FingerprintEnrollIntroStatus>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.pageStatusFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TEST_USER_ID = 33
|
||||
}
|
||||
}
|
@@ -1,421 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.ui.viewmodel;
|
||||
|
||||
import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
|
||||
import static android.hardware.fingerprint.FingerprintManager.ENROLL_FIND_SENSOR;
|
||||
import static android.hardware.fingerprint.FingerprintManager.EnrollReason;
|
||||
import static android.hardware.fingerprint.FingerprintManager.EnrollmentCallback;
|
||||
|
||||
import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.only;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Resources;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintUpdater;
|
||||
import com.android.settings.biometrics.fingerprint.MessageDisplayController;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
|
||||
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 FingerprintEnrollProgressViewModelTest {
|
||||
|
||||
private static final int TEST_USER_ID = 334;
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Mock private Application mApplication;
|
||||
@Mock private Resources mResources;
|
||||
@Mock private FingerprintUpdater mFingerprintUpdater;
|
||||
|
||||
private FingerprintEnrollProgressViewModel mViewModel;
|
||||
private final TestWrapper<CancellationSignal> mCancellationSignalWrapper = new TestWrapper<>();
|
||||
private final TestWrapper<EnrollmentCallback> mCallbackWrapper = new TestWrapper<>();
|
||||
private int mEnrollmentMessageDisplayControllerFlagResId;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mEnrollmentMessageDisplayControllerFlagResId = ApplicationProvider.getApplicationContext()
|
||||
.getResources().getIdentifier("enrollment_message_display_controller_flag", "bool",
|
||||
SETTINGS_PACKAGE_NAME);
|
||||
|
||||
when(mApplication.getResources()).thenReturn(mResources);
|
||||
|
||||
// Not use MessageDisplayController by default
|
||||
when(mResources.getBoolean(mEnrollmentMessageDisplayControllerFlagResId)).thenReturn(false);
|
||||
mViewModel = new FingerprintEnrollProgressViewModel(mApplication, mFingerprintUpdater,
|
||||
TEST_USER_ID);
|
||||
|
||||
mCancellationSignalWrapper.mValue = null;
|
||||
mCallbackWrapper.mValue = null;
|
||||
doAnswer(invocation -> {
|
||||
mCancellationSignalWrapper.mValue = invocation.getArgument(1);
|
||||
mCallbackWrapper.mValue = invocation.getArgument(3);
|
||||
return null;
|
||||
}).when(mFingerprintUpdater).enroll(any(byte[].class), any(CancellationSignal.class),
|
||||
eq(TEST_USER_ID), any(EnrollmentCallback.class), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartFindSensor() {
|
||||
@EnrollReason final int enrollReason = ENROLL_FIND_SENSOR;
|
||||
final byte[] token = new byte[] { 1, 2, 3 };
|
||||
mViewModel.setToken(token);
|
||||
|
||||
// Start enrollment
|
||||
final Object ret = mViewModel.startEnrollment(enrollReason);
|
||||
|
||||
assertThat(ret).isNotNull();
|
||||
verify(mFingerprintUpdater, only()).enroll(eq(token), any(CancellationSignal.class),
|
||||
eq(TEST_USER_ID), any(EnrollmentCallback.class), eq(enrollReason), any());
|
||||
assertThat(mCallbackWrapper.mValue instanceof MessageDisplayController).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartEnrolling() {
|
||||
@EnrollReason final int enrollReason = ENROLL_ENROLL;
|
||||
final byte[] token = new byte[] { 1, 2, 3 };
|
||||
mViewModel.setToken(token);
|
||||
|
||||
// Start enrollment
|
||||
final Object ret = mViewModel.startEnrollment(enrollReason);
|
||||
|
||||
assertThat(ret).isNotNull();
|
||||
verify(mFingerprintUpdater, only()).enroll(eq(token), any(CancellationSignal.class),
|
||||
eq(TEST_USER_ID), any(EnrollmentCallback.class), eq(enrollReason), any());
|
||||
assertThat(mCallbackWrapper.mValue instanceof MessageDisplayController).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartEnrollingWithMessageDisplayController() {
|
||||
// Enable MessageDisplayController and mock handler for it
|
||||
when(mResources.getBoolean(mEnrollmentMessageDisplayControllerFlagResId)).thenReturn(true);
|
||||
when(mApplication.getMainThreadHandler()).thenReturn(new TestHandler());
|
||||
|
||||
@EnrollReason final int enrollReason = ENROLL_ENROLL;
|
||||
final byte[] token = new byte[] { 1, 2, 3 };
|
||||
mViewModel.setToken(token);
|
||||
|
||||
// Start enrollment
|
||||
final Object ret = mViewModel.startEnrollment(enrollReason);
|
||||
|
||||
assertThat(ret).isNotNull();
|
||||
verify(mFingerprintUpdater, only()).enroll(eq(token), any(CancellationSignal.class),
|
||||
eq(TEST_USER_ID), any(MessageDisplayController.class), eq(enrollReason), any());
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
assertThat(mCallbackWrapper.mValue instanceof MessageDisplayController).isTrue();
|
||||
final EnrollmentCallback callback1 = mCallbackWrapper.mValue;
|
||||
|
||||
// Cancel and start again
|
||||
mViewModel.cancelEnrollment();
|
||||
mViewModel.startEnrollment(enrollReason);
|
||||
|
||||
// Shall not use the same MessageDisplayController
|
||||
verify(mFingerprintUpdater, times(2)).enroll(eq(token), any(CancellationSignal.class),
|
||||
eq(TEST_USER_ID), any(MessageDisplayController.class), eq(enrollReason), any());
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
assertThat(callback1).isNotEqualTo(mCallbackWrapper.mValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStartEnrollmentFailBecauseOfNoToken() {
|
||||
// Start enrollment
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_FIND_SENSOR);
|
||||
|
||||
assertThat(ret).isNull();
|
||||
verify(mFingerprintUpdater, never()).enroll(any(byte[].class),
|
||||
any(CancellationSignal.class), anyInt(), any(EnrollmentCallback.class), anyInt(),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelEnrollment() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCancellationSignalWrapper.mValue).isNotNull();
|
||||
|
||||
// Cancel enrollment
|
||||
mViewModel.cancelEnrollment();
|
||||
|
||||
assertThat(mCancellationSignalWrapper.mValue.isCanceled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressUpdate() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Test default value
|
||||
final LiveData<EnrollmentProgress> progressLiveData = mViewModel.getProgressLiveData();
|
||||
EnrollmentProgress progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(-1);
|
||||
assertThat(progress.getRemaining()).isEqualTo(0);
|
||||
|
||||
// Update first progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(25);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(25);
|
||||
|
||||
// Update second progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(20);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(20);
|
||||
|
||||
// Update latest progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(0);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressUpdateClearHelpMessage() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
final LiveData<EnrollmentProgress> progressLiveData = mViewModel.getProgressLiveData();
|
||||
final LiveData<EnrollmentStatusMessage> helpMsgLiveData =
|
||||
mViewModel.getHelpMessageLiveData();
|
||||
|
||||
// Update first progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(25);
|
||||
EnrollmentProgress progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(25);
|
||||
|
||||
// Update help message
|
||||
final int testHelpMsgId = 3;
|
||||
final String testHelpString = "Test Help String";
|
||||
mCallbackWrapper.mValue.onEnrollmentHelp(testHelpMsgId, testHelpString);
|
||||
final EnrollmentStatusMessage helpMsg = helpMsgLiveData.getValue();
|
||||
assertThat(helpMsg).isNotNull();
|
||||
assertThat(helpMsg.getMsgId()).isEqualTo(testHelpMsgId);
|
||||
assertThat(helpMsg.getStr().toString()).isEqualTo(testHelpString);
|
||||
|
||||
// Update second progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(20);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(20);
|
||||
|
||||
// Help message shall be set to null
|
||||
assertThat(helpMsgLiveData.getValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgressUpdateWithMessageDisplayController() {
|
||||
// Enable MessageDisplayController and mock handler for it
|
||||
when(mResources.getBoolean(mEnrollmentMessageDisplayControllerFlagResId)).thenReturn(true);
|
||||
when(mApplication.getMainThreadHandler()).thenReturn(new TestHandler());
|
||||
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
|
||||
// Start enrollment
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Test default value
|
||||
final LiveData<EnrollmentProgress> progressLiveData = mViewModel.getProgressLiveData();
|
||||
EnrollmentProgress progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(-1);
|
||||
assertThat(progress.getRemaining()).isEqualTo(0);
|
||||
|
||||
// Update first progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(25);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(25);
|
||||
|
||||
// Update second progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(20);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(20);
|
||||
|
||||
// Update latest progress
|
||||
mCallbackWrapper.mValue.onEnrollmentProgress(0);
|
||||
progress = progressLiveData.getValue();
|
||||
assertThat(progress).isNotNull();
|
||||
assertThat(progress.getSteps()).isEqualTo(25);
|
||||
assertThat(progress.getRemaining()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetErrorMessageLiveData() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Check default value
|
||||
final LiveData<EnrollmentStatusMessage> liveData = mViewModel.getErrorMessageLiveData();
|
||||
assertThat(liveData.getValue()).isNull();
|
||||
|
||||
// Notify error message
|
||||
final int errMsgId = 3;
|
||||
final String errMsg = "test error message";
|
||||
mCallbackWrapper.mValue.onEnrollmentError(errMsgId, errMsg);
|
||||
final EnrollmentStatusMessage value = liveData.getValue();
|
||||
assertThat(value).isNotNull();
|
||||
assertThat(value.getMsgId()).isEqualTo(errMsgId);
|
||||
assertThat(value.getStr().toString()).isEqualTo(errMsg);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetHelpMessageLiveData() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Check default value
|
||||
final LiveData<EnrollmentStatusMessage> liveData = mViewModel.getHelpMessageLiveData();
|
||||
assertThat(liveData.getValue()).isNull();
|
||||
|
||||
// Notify help message
|
||||
final int errMsgId = 3;
|
||||
final String errMsg = "test error message";
|
||||
mCallbackWrapper.mValue.onEnrollmentHelp(errMsgId, errMsg);
|
||||
final EnrollmentStatusMessage value = liveData.getValue();
|
||||
assertThat(value).isNotNull();
|
||||
assertThat(value.getMsgId()).isEqualTo(errMsgId);
|
||||
assertThat(value.getStr().toString()).isEqualTo(errMsg);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAcquireLiveData() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Check default value
|
||||
final LiveData<Boolean> liveData = mViewModel.getAcquireLiveData();
|
||||
assertThat(liveData.getValue()).isNull();
|
||||
|
||||
// Notify acquire message
|
||||
mCallbackWrapper.mValue.onAcquired(true);
|
||||
assertThat(liveData.getValue()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPointerDownLiveData() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Check default value
|
||||
final LiveData<Integer> liveData = mViewModel.getPointerDownLiveData();
|
||||
assertThat(liveData.getValue()).isNull();
|
||||
|
||||
// Notify acquire message
|
||||
final int value = 33;
|
||||
mCallbackWrapper.mValue.onUdfpsPointerDown(value);
|
||||
assertThat(liveData.getValue()).isEqualTo(value);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetPointerUpLiveData() {
|
||||
// Start enrollment
|
||||
mViewModel.setToken(new byte[] { 1, 2, 3 });
|
||||
final Object ret = mViewModel.startEnrollment(ENROLL_ENROLL);
|
||||
assertThat(ret).isNotNull();
|
||||
assertThat(mCallbackWrapper.mValue).isNotNull();
|
||||
|
||||
// Check default value
|
||||
final LiveData<Integer> liveData = mViewModel.getPointerUpLiveData();
|
||||
assertThat(liveData.getValue()).isNull();
|
||||
|
||||
// Notify acquire message
|
||||
final int value = 44;
|
||||
mCallbackWrapper.mValue.onUdfpsPointerUp(value);
|
||||
assertThat(liveData.getValue()).isEqualTo(value);
|
||||
}
|
||||
|
||||
private static class TestWrapper<T> {
|
||||
T mValue;
|
||||
}
|
||||
|
||||
private static class TestHandler extends Handler {
|
||||
|
||||
TestHandler() {
|
||||
super(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
|
||||
msg.getCallback().run();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,322 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.hardware.fingerprint.FingerprintManager
|
||||
import android.hardware.fingerprint.FingerprintSensorProperties
|
||||
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.biometrics.BiometricEnrollBase
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newAllFalseRequest
|
||||
import com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwRequest
|
||||
import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.newFingerprintRepository
|
||||
import com.android.settings.biometrics2.utils.FingerprintRepositoryUtils.setupFingerprintEnrolledFingerprints
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
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)
|
||||
class FingerprintEnrollmentViewModelTest {
|
||||
|
||||
@get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
private val application: Application
|
||||
get() = ApplicationProvider.getApplicationContext()
|
||||
|
||||
@Mock
|
||||
private lateinit var fingerprintManager: FingerprintManager
|
||||
|
||||
private lateinit var fingerprintRepository: FingerprintRepository
|
||||
private lateinit var viewModel: FingerprintEnrollmentViewModel
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fingerprintRepository = newFingerprintRepository(
|
||||
fingerprintManager,
|
||||
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
|
||||
5
|
||||
)
|
||||
viewModel = FingerprintEnrollmentViewModel(
|
||||
application,
|
||||
fingerprintRepository,
|
||||
newAllFalseRequest(application)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetRequest() {
|
||||
assertThat(viewModel.request).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsWaitingActivityResultDefaultFalse() {
|
||||
assertThat(viewModel.isWaitingActivityResult.value).isFalse()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testOverrideActivityResult_shallKeepNullIntent_woChallengeExtra() {
|
||||
val retResult = viewModel.getOverrideActivityResult(
|
||||
ActivityResult(22, null), null
|
||||
)
|
||||
assertThat(retResult).isNotNull()
|
||||
assertThat(retResult.data).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOverrideActivityResult_shallKeepNullIntent_noIntent_woChallengeExtra() {
|
||||
val intent = Intent()
|
||||
val retResult = viewModel.getOverrideActivityResult(
|
||||
ActivityResult(33, intent), null
|
||||
)
|
||||
assertThat(retResult).isNotNull()
|
||||
assertThat(retResult.data).isEqualTo(intent)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOverrideActivityResult_shallKeepNull_woAdded_woIntent_withChallenge() {
|
||||
val extra = Bundle()
|
||||
extra.putString("test1", "test123")
|
||||
|
||||
val retResult = viewModel.getOverrideActivityResult(
|
||||
ActivityResult(33, null), extra
|
||||
)
|
||||
|
||||
assertThat(retResult).isNotNull()
|
||||
assertThat(retResult.data).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOverrideActivityResult_shallCreateNew_woIntent_withChallenge() {
|
||||
val key1 = "test1"
|
||||
val key2 = "test2"
|
||||
val extra = Bundle().apply {
|
||||
putString(key1, "test123")
|
||||
putInt(key2, 9999)
|
||||
}
|
||||
|
||||
viewModel.isNewFingerprintAdded = true
|
||||
|
||||
val retResult = viewModel.getOverrideActivityResult(
|
||||
ActivityResult(33, null), extra
|
||||
)
|
||||
assertThat(retResult).isNotNull()
|
||||
|
||||
val retIntent = retResult.data
|
||||
assertThat(retIntent).isNotNull()
|
||||
|
||||
val retExtra = retIntent!!.extras
|
||||
assertThat(retExtra).isNotNull()
|
||||
assertThat(retExtra!!.size).isEqualTo(extra.size)
|
||||
assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1))
|
||||
assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOverrideActivityResult_shallNotMerge_nonAdded_woIntent_withChallenge() {
|
||||
val extra = Bundle().apply {
|
||||
putString("test2", "test123")
|
||||
}
|
||||
|
||||
val key2 = "test2"
|
||||
val intent = Intent().apply {
|
||||
putExtra(key2, 3456L)
|
||||
}
|
||||
|
||||
val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra)
|
||||
|
||||
assertThat(retResult).isNotNull()
|
||||
|
||||
val retIntent = retResult.data
|
||||
assertThat(retIntent).isNotNull()
|
||||
|
||||
val retExtra = retIntent!!.extras
|
||||
assertThat(retExtra).isNotNull()
|
||||
assertThat(retExtra!!.size).isEqualTo(intent.extras!!.size)
|
||||
assertThat(retExtra.getString(key2)).isEqualTo(intent.extras!!.getString(key2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOverrideActivityResult_shallMerge_added_woIntent_withChallenge() {
|
||||
val key1 = "test1"
|
||||
val key2 = "test2"
|
||||
val extra = Bundle().apply {
|
||||
putString(key1, "test123")
|
||||
putInt(key2, 9999)
|
||||
}
|
||||
|
||||
val key3 = "test3"
|
||||
val intent = Intent().apply {
|
||||
putExtra(key3, 3456L)
|
||||
}
|
||||
|
||||
viewModel.isNewFingerprintAdded = true
|
||||
|
||||
val retResult = viewModel.getOverrideActivityResult(ActivityResult(33, intent), extra)
|
||||
assertThat(retResult).isNotNull()
|
||||
|
||||
val retIntent = retResult.data
|
||||
assertThat(retIntent).isNotNull()
|
||||
|
||||
val retExtra = retIntent!!.extras
|
||||
assertThat(retExtra).isNotNull()
|
||||
assertThat(retExtra!!.size).isEqualTo(extra.size + intent.extras!!.size)
|
||||
assertThat(retExtra.getString(key1)).isEqualTo(extra.getString(key1))
|
||||
assertThat(retExtra.getInt(key2)).isEqualTo(extra.getInt(key2))
|
||||
assertThat(retExtra.getLong(key3)).isEqualTo(intent.extras!!.getLong(key3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testIsMaxEnrolledReached() {
|
||||
val uid = 100
|
||||
fingerprintRepository = newFingerprintRepository(
|
||||
fingerprintManager,
|
||||
FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
|
||||
3
|
||||
)
|
||||
viewModel = FingerprintEnrollmentViewModel(
|
||||
application,
|
||||
fingerprintRepository,
|
||||
newAllFalseRequest(application)
|
||||
)
|
||||
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 0)
|
||||
assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse()
|
||||
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 1)
|
||||
assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse()
|
||||
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 2)
|
||||
assertThat(viewModel.isMaxEnrolledReached(uid)).isFalse()
|
||||
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 3)
|
||||
assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue()
|
||||
|
||||
setupFingerprintEnrolledFingerprints(fingerprintManager, uid, 4)
|
||||
assertThat(viewModel.isMaxEnrolledReached(uid)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSetResultFlow_defaultEmpty() = runTest {
|
||||
val activityResults = listOfSetResultFlow()
|
||||
|
||||
runCurrent()
|
||||
|
||||
assertThat(activityResults.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckFinishActivityDuringOnPause_doNothingIfIsSuw() = runTest {
|
||||
viewModel = FingerprintEnrollmentViewModel(
|
||||
application,
|
||||
fingerprintRepository,
|
||||
newIsSuwRequest(application)
|
||||
)
|
||||
|
||||
val activityResults = listOfSetResultFlow()
|
||||
|
||||
viewModel.checkFinishActivityDuringOnPause(
|
||||
isActivityFinishing = false,
|
||||
isChangingConfigurations = false,
|
||||
scope = this
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(activityResults.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckFinishActivityDuringOnPause_doNothingIfIsWaitingActivity() = runTest {
|
||||
val activityResults = listOfSetResultFlow()
|
||||
|
||||
viewModel.isWaitingActivityResult.value = true
|
||||
viewModel.checkFinishActivityDuringOnPause(
|
||||
isActivityFinishing = false,
|
||||
isChangingConfigurations = false,
|
||||
scope = this
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(activityResults.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckFinishActivityDuringOnPause_doNothingIfIsActivityFinishing() = runTest {
|
||||
val activityResults = listOfSetResultFlow()
|
||||
|
||||
viewModel.checkFinishActivityDuringOnPause(
|
||||
isActivityFinishing = true,
|
||||
isChangingConfigurations = false,
|
||||
scope = this
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(activityResults.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckFinishActivityDuringOnPause_doNothingIfIsChangingConfigurations() = runTest {
|
||||
val activityResults = listOfSetResultFlow()
|
||||
|
||||
viewModel.checkFinishActivityDuringOnPause(
|
||||
isActivityFinishing = false,
|
||||
isChangingConfigurations = true,
|
||||
scope = this
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(activityResults.size).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheckFinishActivityDuringOnPause_defaultFinishSelf() = runTest {
|
||||
val activityResults = listOfSetResultFlow()
|
||||
|
||||
viewModel.checkFinishActivityDuringOnPause(
|
||||
isActivityFinishing = false,
|
||||
isChangingConfigurations = false,
|
||||
scope = backgroundScope
|
||||
)
|
||||
runCurrent()
|
||||
|
||||
assertThat(activityResults.size).isEqualTo(1)
|
||||
assertThat(activityResults[0].resultCode).isEqualTo(BiometricEnrollBase.RESULT_TIMEOUT)
|
||||
assertThat(activityResults[0].data).isEqualTo(null)
|
||||
}
|
||||
|
||||
private fun TestScope.listOfSetResultFlow(): List<ActivityResult> =
|
||||
mutableListOf<ActivityResult>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.setResultFlow.toList(it)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.text.TextUtils
|
||||
import com.android.settings.biometrics.BiometricEnrollBase
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest
|
||||
import com.google.android.setupcompat.util.WizardManagerHelper
|
||||
|
||||
object EnrollmentRequestUtils {
|
||||
@JvmStatic
|
||||
fun newAllFalseRequest(context: Context): EnrollmentRequest {
|
||||
return newRequest(
|
||||
context = context,
|
||||
isSuw = false,
|
||||
isSuwDeferred = false,
|
||||
isSuwPortal = false,
|
||||
isSuwSuggestedActionFlow = false,
|
||||
isSuwFirstRun = false,
|
||||
isFromSettingsSummery = false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newIsSuwRequest(context: Context): EnrollmentRequest {
|
||||
return newRequest(
|
||||
context = context,
|
||||
isSuw = true,
|
||||
isSuwDeferred = false,
|
||||
isSuwPortal = false,
|
||||
isSuwSuggestedActionFlow = false,
|
||||
isSuwFirstRun = false,
|
||||
isFromSettingsSummery = false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newIsSuwDeferredRequest(context: Context): EnrollmentRequest {
|
||||
return newRequest(
|
||||
context = context,
|
||||
isSuw = true,
|
||||
isSuwDeferred = true,
|
||||
isSuwPortal = false,
|
||||
isSuwSuggestedActionFlow = false,
|
||||
isSuwFirstRun = false,
|
||||
isFromSettingsSummery = false, null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newIsSuwPortalRequest(context: Context): EnrollmentRequest {
|
||||
return newRequest(
|
||||
context = context,
|
||||
isSuw = true,
|
||||
isSuwDeferred = false,
|
||||
isSuwPortal = true,
|
||||
isSuwSuggestedActionFlow = false,
|
||||
isSuwFirstRun = false,
|
||||
isFromSettingsSummery = false)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newIsSuwSuggestedActionFlowRequest(
|
||||
context: Context
|
||||
): EnrollmentRequest {
|
||||
return newRequest(
|
||||
context = context,
|
||||
isSuw = true,
|
||||
isSuwDeferred = false,
|
||||
isSuwPortal = false,
|
||||
isSuwSuggestedActionFlow = true,
|
||||
isSuwFirstRun = false,
|
||||
isFromSettingsSummery = false)
|
||||
}
|
||||
|
||||
fun newRequest(
|
||||
context: Context,
|
||||
isSuw: Boolean,
|
||||
isSuwDeferred: Boolean,
|
||||
isSuwPortal: Boolean,
|
||||
isSuwSuggestedActionFlow: Boolean,
|
||||
isSuwFirstRun: Boolean,
|
||||
isFromSettingsSummery: Boolean,
|
||||
theme: String? = null
|
||||
): EnrollmentRequest {
|
||||
val i = Intent()
|
||||
i.putExtra(WizardManagerHelper.EXTRA_IS_SETUP_FLOW, isSuw)
|
||||
i.putExtra(WizardManagerHelper.EXTRA_IS_DEFERRED_SETUP, isSuwDeferred)
|
||||
i.putExtra(WizardManagerHelper.EXTRA_IS_PORTAL_SETUP, isSuwPortal)
|
||||
i.putExtra(WizardManagerHelper.EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW, isSuwSuggestedActionFlow)
|
||||
i.putExtra(WizardManagerHelper.EXTRA_IS_FIRST_RUN, isSuwFirstRun)
|
||||
i.putExtra(BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY, isFromSettingsSummery)
|
||||
if (!TextUtils.isEmpty(theme)) {
|
||||
i.putExtra(WizardManagerHelper.EXTRA_THEME, theme)
|
||||
}
|
||||
return EnrollmentRequest(i, context, true)
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.biometrics2.utils;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
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 android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class FingerprintRepositoryUtils {
|
||||
|
||||
public static void setupSuwMaxFingerprintsEnrollable(
|
||||
@NonNull Context context,
|
||||
@NonNull Resources mockedResources,
|
||||
int numOfFp) {
|
||||
final int resId = context.getResources().getIdentifier("suw_max_fingerprints_enrollable",
|
||||
"integer", context.getPackageName());
|
||||
when(mockedResources.getInteger(resId)).thenReturn(numOfFp);
|
||||
}
|
||||
|
||||
public static FingerprintRepository newFingerprintRepository(
|
||||
@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 */));
|
||||
doAnswer(invocation -> {
|
||||
final IFingerprintAuthenticatorsRegisteredCallback callback =
|
||||
invocation.getArgument(0);
|
||||
callback.onAllAuthenticatorsRegistered(props);
|
||||
return null;
|
||||
}).when(mockedFingerprintManager).addAuthenticatorsRegisteredCallback(any());
|
||||
return new FingerprintRepository(mockedFingerprintManager);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@@ -25,7 +25,6 @@ import com.android.settings.accounts.AccountFeatureProvider;
|
||||
import com.android.settings.applications.ApplicationFeatureProvider;
|
||||
import com.android.settings.biometrics.face.FaceFeatureProvider;
|
||||
import com.android.settings.biometrics.fingerprint.FingerprintFeatureProvider;
|
||||
import com.android.settings.biometrics2.factory.BiometricsRepositoryProvider;
|
||||
import com.android.settings.bluetooth.BluetoothFeatureProvider;
|
||||
import com.android.settings.connecteddevice.fastpair.FastPairFeatureProvider;
|
||||
import com.android.settings.connecteddevice.stylus.StylusFeatureProvider;
|
||||
@@ -82,7 +81,6 @@ public class FakeFeatureFactory extends FeatureFactory {
|
||||
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
|
||||
public final FaceFeatureProvider mFaceFeatureProvider;
|
||||
public final FingerprintFeatureProvider mFingerprintFeatureProvider;
|
||||
public final BiometricsRepositoryProvider mBiometricsRepositoryProvider;
|
||||
|
||||
public PanelFeatureProvider panelFeatureProvider;
|
||||
public SlicesFeatureProvider slicesFeatureProvider;
|
||||
@@ -140,7 +138,6 @@ public class FakeFeatureFactory extends FeatureFactory {
|
||||
mBluetoothFeatureProvider = mock(BluetoothFeatureProvider.class);
|
||||
mFaceFeatureProvider = mock(FaceFeatureProvider.class);
|
||||
mFingerprintFeatureProvider = mock(FingerprintFeatureProvider.class);
|
||||
mBiometricsRepositoryProvider = mock(BiometricsRepositoryProvider.class);
|
||||
wifiTrackerLibProvider = mock(WifiTrackerLibProvider.class);
|
||||
securitySettingsFeatureProvider = mock(SecuritySettingsFeatureProvider.class);
|
||||
mAccessibilitySearchFeatureProvider = mock(AccessibilitySearchFeatureProvider.class);
|
||||
@@ -272,11 +269,6 @@ public class FakeFeatureFactory extends FeatureFactory {
|
||||
return mFingerprintFeatureProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiometricsRepositoryProvider getBiometricsRepositoryProvider() {
|
||||
return mBiometricsRepositoryProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WifiTrackerLibProvider getWifiTrackerLibProvider() {
|
||||
return wifiTrackerLibProvider;
|
||||
|
Reference in New Issue
Block a user