Merge "Remove package biometric2 from AOSP settings" into main

This commit is contained in:
Vincent Wang
2024-07-15 05:00:06 +00:00
committed by Android (Google) Code Review
61 changed files with 7 additions and 10449 deletions

View File

@@ -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"

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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.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);

View File

@@ -1,4 +0,0 @@
# The Android Biometric team should approve all changes to biometrics2 subdirectories.
set noparent
include /src/com/android/settings/biometrics/OWNERS

View File

@@ -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();
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}
}

View File

@@ -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}"
}
}

View File

@@ -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}")
}
}

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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) }

View File

@@ -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)
}
}

View File

@@ -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)
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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"
}
}

View File

@@ -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
}
}
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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();
}
}

View File

@@ -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"
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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();
});
}
}

View File

@@ -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.
*/

View File

@@ -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()
}

View File

@@ -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;

View File

@@ -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

View File

@@ -1 +0,0 @@
include /src/com/android/settings/biometrics/OWNERS

View File

@@ -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);
}
}

View File

@@ -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]
)
}
}
}
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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)
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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);
}
}

View File

@@ -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;