Merge "[BiometricsV2] Refactor EnrollIntroViewModel" into main
This commit is contained in:
committed by
Android (Google) Code Review
commit
317bfca133
@@ -28,12 +28,15 @@ 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.LiveData
|
||||
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
|
||||
@@ -47,6 +50,8 @@ 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 kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.function.Supplier
|
||||
|
||||
/**
|
||||
@@ -59,12 +64,7 @@ class FingerprintEnrollIntroFragment : Fragment() {
|
||||
|
||||
private var _viewModel: FingerprintEnrollIntroViewModel? = null
|
||||
private val viewModel: FingerprintEnrollIntroViewModel
|
||||
get() {
|
||||
if (_viewModel == null) {
|
||||
_viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
|
||||
}
|
||||
return _viewModel!!
|
||||
}
|
||||
get() = _viewModel!!
|
||||
|
||||
private var introView: GlifLayout? = null
|
||||
|
||||
@@ -73,10 +73,18 @@ class FingerprintEnrollIntroFragment : Fragment() {
|
||||
private var secondaryFooterButton: FooterButton? = null
|
||||
|
||||
private val onNextClickListener =
|
||||
View.OnClickListener { _: View? -> viewModel.onNextButtonClick() }
|
||||
View.OnClickListener { _: View? ->
|
||||
activity?.lifecycleScope?.let {
|
||||
viewModel.onNextButtonClick(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val onSkipOrCancelClickListener =
|
||||
View.OnClickListener { _: View? -> viewModel.onSkipOrCancelButtonClick() }
|
||||
View.OnClickListener { _: View? ->
|
||||
activity?.lifecycleScope?.let {
|
||||
viewModel.onSkipOrCancelButtonClick(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -95,7 +103,7 @@ class FingerprintEnrollIntroFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
requireActivity().bindFingerprintEnrollIntroView(
|
||||
view = introView!!,
|
||||
canAssumeUdfps = viewModel.canAssumeUdfps(),
|
||||
canAssumeUdfps = viewModel.canAssumeUdfps,
|
||||
isBiometricUnlockDisabledByAdmin = viewModel.isBiometricUnlockDisabledByAdmin,
|
||||
isParentalConsentRequired = viewModel.isParentalConsentRequired,
|
||||
descriptionDisabledByAdminSupplier = { getDescriptionDisabledByAdmin(view.context) }
|
||||
@@ -105,9 +113,10 @@ class FingerprintEnrollIntroFragment : Fragment() {
|
||||
override fun onStart() {
|
||||
val context: Context = requireContext()
|
||||
val footerBarMixin: FooterBarMixin = footerBarMixin
|
||||
viewModel.updateEnrollableStatus(lifecycleScope)
|
||||
initPrimaryFooterButton(context, footerBarMixin)
|
||||
initSecondaryFooterButton(context, footerBarMixin)
|
||||
observePageStatusLiveDataIfNeed()
|
||||
collectPageStatusFlowIfNeed()
|
||||
super.onStart()
|
||||
}
|
||||
|
||||
@@ -152,46 +161,41 @@ class FingerprintEnrollIntroFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun observePageStatusLiveDataIfNeed() {
|
||||
val statusLiveData: LiveData<FingerprintEnrollIntroStatus> =
|
||||
viewModel.pageStatusLiveData
|
||||
val status: FingerprintEnrollIntroStatus? = statusLiveData.value
|
||||
|
||||
if (DEBUG) {
|
||||
Log.e(
|
||||
TAG, "observePageStatusLiveDataIfNeed() requireScrollWithButton, status:"
|
||||
+ status
|
||||
)
|
||||
}
|
||||
|
||||
if (status != null && (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)
|
||||
return
|
||||
}
|
||||
|
||||
introView!!.getMixin(RequireScrollMixin::class.java).let {
|
||||
it.requireScrollWithButton(
|
||||
requireActivity(),
|
||||
primaryFooterButton!!,
|
||||
moreButtonTextRes,
|
||||
onNextClickListener
|
||||
)
|
||||
it.setOnRequireScrollStateChangedListener { scrollNeeded: Boolean ->
|
||||
viewModel.setHasScrolledToBottom(!scrollNeeded)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
statusLiveData.observe(this) { newStatus: FingerprintEnrollIntroStatus ->
|
||||
updateFooterButtons(newStatus)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
_viewModel = null
|
||||
_viewModel = viewModelProvider[FingerprintEnrollIntroViewModel::class.java]
|
||||
super.onAttach(context)
|
||||
}
|
||||
|
||||
@@ -319,4 +323,7 @@ fun FragmentActivity.bindFingerprintEnrollIntroView(
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
view.findViewById<ScrollView>(R.id.sud_scroll_view)?.importantForAccessibility =
|
||||
View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||
}
|
||||
|
@@ -68,11 +68,8 @@ import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishView
|
||||
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.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FingerprintEnrollIntroAction
|
||||
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
|
||||
@@ -129,13 +126,6 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
|
||||
|
||||
private var isFirstFragmentAdded = false
|
||||
|
||||
private val introActionObserver: Observer<Int> = Observer<Int> { action ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "introActionObserver($action)")
|
||||
}
|
||||
action?.let { onIntroAction(it) }
|
||||
}
|
||||
|
||||
private val findSensorActionObserver: Observer<Int> = Observer<Int> { action ->
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "findSensorActionObserver($action)")
|
||||
@@ -290,12 +280,10 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
|
||||
if (request.isSkipIntro || request.isSkipFindSensor) {
|
||||
return
|
||||
}
|
||||
introViewModel.let {
|
||||
// Clear ActionLiveData in FragmentViewModel to prevent getting previous action during
|
||||
// recreate, like press 'Agree' then press 'back' in FingerprintEnrollFindSensor
|
||||
// activity.
|
||||
it.clearActionLiveData()
|
||||
it.actionLiveData.observe(this, introActionObserver)
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
introViewModel.actionFlow.collect(this@FingerprintEnrollmentActivity::onIntroAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,23 +468,20 @@ open class FingerprintEnrollmentActivity : FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onIntroAction(@FingerprintEnrollIntroAction action: Int) {
|
||||
private fun onIntroAction(action: FingerprintEnrollIntroAction) {
|
||||
Log.d(TAG, "onIntroAction($action)")
|
||||
when (action) {
|
||||
FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH -> {
|
||||
onSetActivityResult(
|
||||
ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null)
|
||||
)
|
||||
FingerprintEnrollIntroAction.DONE_AND_FINISH -> {
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_FINISHED, null))
|
||||
return
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL -> {
|
||||
onSetActivityResult(
|
||||
ActivityResult(BiometricEnrollBase.RESULT_SKIP, null)
|
||||
)
|
||||
FingerprintEnrollIntroAction.SKIP_OR_CANCEL -> {
|
||||
onSetActivityResult(ActivityResult(BiometricEnrollBase.RESULT_SKIP, null))
|
||||
return
|
||||
}
|
||||
|
||||
FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL -> {
|
||||
FingerprintEnrollIntroAction.CONTINUE_ENROLL -> {
|
||||
startFindSensorFragment()
|
||||
}
|
||||
}
|
||||
|
@@ -1,206 +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.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
|
||||
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK;
|
||||
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN;
|
||||
|
||||
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.MediatorLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollable;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Fingerprint intro onboarding page view model implementation
|
||||
*/
|
||||
public class FingerprintEnrollIntroViewModel extends AndroidViewModel {
|
||||
|
||||
private static final String TAG = "FingerprintEnrollIntroViewModel";
|
||||
private static final boolean HAS_SCROLLED_TO_BOTTOM_DEFAULT = false;
|
||||
private static final FingerprintEnrollable ENROLLABLE_STATUS_DEFAULT =
|
||||
FINGERPRINT_ENROLLABLE_UNKNOWN;
|
||||
|
||||
/**
|
||||
* User clicks 'Done' button on this page
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH = 0;
|
||||
|
||||
/**
|
||||
* User clicks 'Agree' button on this page
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL = 1;
|
||||
|
||||
/**
|
||||
* User clicks 'Skip' button on this page
|
||||
*/
|
||||
public static final int FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL = 2;
|
||||
|
||||
@IntDef(prefix = { "FINGERPRINT_ENROLL_INTRO_ACTION_" }, value = {
|
||||
FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH,
|
||||
FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL,
|
||||
FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL
|
||||
})
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
public @interface FingerprintEnrollIntroAction {}
|
||||
|
||||
@NonNull private final FingerprintRepository mFingerprintRepository;
|
||||
|
||||
private final MutableLiveData<Boolean> mHasScrolledToBottomLiveData =
|
||||
new MutableLiveData<>(HAS_SCROLLED_TO_BOTTOM_DEFAULT);
|
||||
private final MutableLiveData<FingerprintEnrollable> mEnrollableStatusLiveData =
|
||||
new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT);
|
||||
private final MediatorLiveData<FingerprintEnrollIntroStatus> mPageStatusLiveData =
|
||||
new MediatorLiveData<>();
|
||||
private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
|
||||
private final int mUserId;
|
||||
@NonNull private final EnrollmentRequest mRequest;
|
||||
|
||||
public FingerprintEnrollIntroViewModel(@NonNull Application application,
|
||||
@NonNull FingerprintRepository fingerprintRepository,
|
||||
@NonNull EnrollmentRequest request, int userId) {
|
||||
super(application);
|
||||
mFingerprintRepository = fingerprintRepository;
|
||||
mRequest = request;
|
||||
mUserId = userId;
|
||||
|
||||
mPageStatusLiveData.addSource(
|
||||
mEnrollableStatusLiveData,
|
||||
enrollable -> {
|
||||
final Boolean toBottomValue = mHasScrolledToBottomLiveData.getValue();
|
||||
final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
|
||||
toBottomValue != null ? toBottomValue : HAS_SCROLLED_TO_BOTTOM_DEFAULT,
|
||||
enrollable);
|
||||
mPageStatusLiveData.setValue(status);
|
||||
});
|
||||
mPageStatusLiveData.addSource(
|
||||
mHasScrolledToBottomLiveData,
|
||||
hasScrolledToBottom -> {
|
||||
final FingerprintEnrollable enrollableValue =
|
||||
mEnrollableStatusLiveData.getValue();
|
||||
final FingerprintEnrollIntroStatus status = new FingerprintEnrollIntroStatus(
|
||||
hasScrolledToBottom,
|
||||
enrollableValue != null ? enrollableValue : ENROLLABLE_STATUS_DEFAULT);
|
||||
mPageStatusLiveData.setValue(status);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enrollment request
|
||||
*/
|
||||
public EnrollmentRequest getRequest() {
|
||||
return mRequest;
|
||||
}
|
||||
|
||||
private void updateEnrollableStatus() {
|
||||
final int num = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
|
||||
final int max =
|
||||
mRequest.isSuw() && !mRequest.isAfterSuwOrSuwSuggestedAction()
|
||||
? mFingerprintRepository.getMaxFingerprintsInSuw(getApplication().getResources())
|
||||
: mFingerprintRepository.getMaxFingerprints();
|
||||
mEnrollableStatusLiveData.postValue(num >= max
|
||||
? FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
: FINGERPRINT_ENROLLABLE_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enrollable status and hasScrollToBottom live data
|
||||
*/
|
||||
public LiveData<FingerprintEnrollIntroStatus> getPageStatusLiveData() {
|
||||
updateEnrollableStatus();
|
||||
return mPageStatusLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear user's action live data
|
||||
*/
|
||||
public void clearActionLiveData() {
|
||||
mActionLiveData.setValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's action live data (like clicking Agree, Skip, or Done)
|
||||
*/
|
||||
public LiveData<Integer> getActionLiveData() {
|
||||
return mActionLiveData;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first sensor type is UDFPS sensor or not
|
||||
*/
|
||||
public boolean canAssumeUdfps() {
|
||||
return mFingerprintRepository.canAssumeUdfps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update onboarding intro page has scrolled to bottom
|
||||
*/
|
||||
public void setHasScrolledToBottom(boolean value) {
|
||||
mHasScrolledToBottomLiveData.postValue(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parental consent required or not during enrollment process
|
||||
*/
|
||||
public boolean isParentalConsentRequired() {
|
||||
return mFingerprintRepository.isParentalConsentRequired(getApplication());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fingerprint is disable by admin or not
|
||||
*/
|
||||
public boolean isBiometricUnlockDisabledByAdmin() {
|
||||
return mFingerprintRepository.isDisabledByAdmin(getApplication(), mUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicks next button
|
||||
*/
|
||||
public void onNextButtonClick() {
|
||||
final FingerprintEnrollable status = mEnrollableStatusLiveData.getValue();
|
||||
switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) {
|
||||
case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX:
|
||||
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
|
||||
break;
|
||||
case FINGERPRINT_ENROLLABLE_OK:
|
||||
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "fail to click next, enrolled:" + status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User clicks skip/cancel button
|
||||
*/
|
||||
public void onSkipOrCancelButtonClick() {
|
||||
mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
|
||||
}
|
||||
}
|
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (C) 2023 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.settings.biometrics2.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollable
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.CONTINUE_ENROLL
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.DONE_AND_FINISH
|
||||
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroAction.SKIP_OR_CANCEL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Fingerprint intro onboarding page view model implementation */
|
||||
class FingerprintEnrollIntroViewModel(
|
||||
application: Application,
|
||||
private val fingerprintRepository: FingerprintRepository,
|
||||
val request: EnrollmentRequest,
|
||||
private val userId: Int
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
/** User's action flow (like clicking Agree, Skip, or Done) */
|
||||
private val _actionFlow = MutableSharedFlow<FingerprintEnrollIntroAction>()
|
||||
val actionFlow: SharedFlow<FingerprintEnrollIntroAction>
|
||||
get() = _actionFlow.asSharedFlow()
|
||||
|
||||
private fun getEnrollableStatus(): FingerprintEnrollable {
|
||||
val num = fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId)
|
||||
val max =
|
||||
if (request.isSuw && !request.isAfterSuwOrSuwSuggestedAction)
|
||||
fingerprintRepository.getMaxFingerprintsInSuw(
|
||||
getApplication<Application>().resources
|
||||
)
|
||||
else
|
||||
fingerprintRepository.maxFingerprints
|
||||
return if (num >= max)
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
else
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK
|
||||
}
|
||||
|
||||
private val hasScrolledToBottomFlow = MutableStateFlow(HAS_SCROLLED_TO_BOTTOM_DEFAULT)
|
||||
private val enrollableStatusFlow = MutableStateFlow(getEnrollableStatus())
|
||||
|
||||
/** Enrollable status and hasScrollToBottom live data */
|
||||
val pageStatusFlow: Flow<FingerprintEnrollIntroStatus> =
|
||||
hasScrolledToBottomFlow.combine(enrollableStatusFlow) {
|
||||
hasScrolledToBottom: Boolean, enrollableStatus: FingerprintEnrollable ->
|
||||
FingerprintEnrollIntroStatus(hasScrolledToBottom, enrollableStatus)
|
||||
}
|
||||
|
||||
fun updateEnrollableStatus(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
enrollableStatusFlow.emit(getEnrollableStatus())
|
||||
}
|
||||
}
|
||||
|
||||
/** The first sensor type is UDFPS sensor or not */
|
||||
val canAssumeUdfps: Boolean
|
||||
get() = fingerprintRepository.canAssumeUdfps()
|
||||
|
||||
/** Update onboarding intro page has scrolled to bottom */
|
||||
fun setHasScrolledToBottom(value: Boolean, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
hasScrolledToBottomFlow.emit(value)
|
||||
}
|
||||
}
|
||||
|
||||
/** Get parental consent required or not during enrollment process */
|
||||
val isParentalConsentRequired: Boolean
|
||||
get() = fingerprintRepository.isParentalConsentRequired(getApplication())
|
||||
|
||||
/** Get fingerprint is disable by admin or not */
|
||||
val isBiometricUnlockDisabledByAdmin: Boolean
|
||||
get() = fingerprintRepository.isDisabledByAdmin(getApplication(), userId)
|
||||
|
||||
/**
|
||||
* User clicks next button
|
||||
*/
|
||||
fun onNextButtonClick(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
when (val status = enrollableStatusFlow.value) {
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX ->
|
||||
_actionFlow.emit(DONE_AND_FINISH)
|
||||
|
||||
FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK ->
|
||||
_actionFlow.emit(CONTINUE_ENROLL)
|
||||
|
||||
else -> Log.w(TAG, "fail to click next, enrolled:$status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** User clicks skip/cancel button */
|
||||
fun onSkipOrCancelButtonClick(scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
_actionFlow.emit(SKIP_OR_CANCEL)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FingerprintEnrollIntroViewModel"
|
||||
private const val HAS_SCROLLED_TO_BOTTOM_DEFAULT = false
|
||||
private val ENROLLABLE_STATUS_DEFAULT = FingerprintEnrollable.FINGERPRINT_ENROLLABLE_UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
enum class FingerprintEnrollIntroAction {
|
||||
/** User clicks 'Done' button on this page */
|
||||
DONE_AND_FINISH,
|
||||
/** User clicks 'Agree' button on this page */
|
||||
CONTINUE_ENROLL,
|
||||
/** User clicks 'Skip' button on this page */
|
||||
SKIP_OR_CANCEL
|
||||
}
|
@@ -1,328 +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.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX;
|
||||
import static com.android.settings.biometrics2.ui.model.FingerprintEnrollable.FINGERPRINT_ENROLLABLE_OK;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH;
|
||||
import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL;
|
||||
import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newAllFalseRequest;
|
||||
import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwDeferredRequest;
|
||||
import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwPortalRequest;
|
||||
import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwRequest;
|
||||
import static com.android.settings.biometrics2.utils.EnrollmentRequestUtils.newIsSuwSuggestedActionFlowRequest;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Resources;
|
||||
import android.hardware.fingerprint.FingerprintManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
|
||||
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
|
||||
import com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus;
|
||||
import com.android.settings.testutils.InstantTaskExecutorRule;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FingerprintEnrollIntroViewModelTest {
|
||||
|
||||
private static final int TEST_USER_ID = 33;
|
||||
|
||||
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
|
||||
@Rule public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
@Mock private Resources mResources;
|
||||
@Mock private FingerprintManager mFingerprintManager;
|
||||
|
||||
private Application mApplication;
|
||||
|
||||
private FingerprintEnrollIntroViewModel newFingerprintEnrollIntroViewModel(
|
||||
@NonNull FingerprintRepository fingerprintRepository,
|
||||
@NonNull EnrollmentRequest enrollmentRequest) {
|
||||
final FingerprintEnrollIntroViewModel viewModel =
|
||||
new FingerprintEnrollIntroViewModel(mApplication, fingerprintRepository,
|
||||
enrollmentRequest, TEST_USER_ID);
|
||||
// MediatorLiveData won't update itself unless observed
|
||||
viewModel.getPageStatusLiveData().observeForever(event -> {});
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mApplication = ApplicationProvider.getApplicationContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageStatusLiveDataDefaultValue() {
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(mApplication));
|
||||
final FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.hasScrollToBottom()).isFalse();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageStatusLiveDataRefreshWhenRefetch() {
|
||||
final FingerprintRepository repository = newFingerprintRepository(mFingerprintManager,
|
||||
TYPE_UDFPS_OPTICAL, 1);
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
repository,
|
||||
newAllFalseRequest(mApplication));
|
||||
FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.hasScrollToBottom()).isFalse();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
|
||||
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 1);
|
||||
|
||||
// Refetch PageStatusLiveData
|
||||
status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.hasScrollToBottom()).isFalse();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearActionLiveData() {
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(mApplication));
|
||||
|
||||
final MutableLiveData<Integer> actionLiveData =
|
||||
(MutableLiveData<Integer>) viewModel.getActionLiveData();
|
||||
actionLiveData.postValue(1);
|
||||
assertThat(actionLiveData.getValue()).isEqualTo(1);
|
||||
|
||||
viewModel.clearActionLiveData();
|
||||
|
||||
assertThat(actionLiveData.getValue()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEnrollmentRequest() {
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(mApplication));
|
||||
|
||||
assertThat(viewModel.getRequest()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusOk_isSuw() {
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 0);
|
||||
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
|
||||
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(mApplication));
|
||||
final FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusReachMax_isSuw() {
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 1);
|
||||
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
|
||||
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(mApplication));
|
||||
final FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusOk_isNotSuw() {
|
||||
testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusOk_isSuwPortal() {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(mApplication));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(
|
||||
newIsSuwSuggestedActionFlowRequest(mApplication));
|
||||
}
|
||||
|
||||
private void testOnStartToUpdateEnrollableStatusOk(@NonNull EnrollmentRequest request) {
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 0);
|
||||
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
request);
|
||||
FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
|
||||
}
|
||||
|
||||
private void testOnStartToUpdateEnrollableStatusReachMax(@NonNull EnrollmentRequest request) {
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 5);
|
||||
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
request);
|
||||
FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsParentalConsentRequired() {
|
||||
// We shall not mock FingerprintRepository, but
|
||||
// FingerprintRepository.isParentalConsentRequired() calls static method inside, we can't
|
||||
// mock static method
|
||||
final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
|
||||
final FingerprintEnrollIntroViewModel viewModel = new FingerprintEnrollIntroViewModel(
|
||||
mApplication, fingerprintRepository, newAllFalseRequest(mApplication),
|
||||
TEST_USER_ID);
|
||||
|
||||
when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(true);
|
||||
assertThat(viewModel.isParentalConsentRequired()).isEqualTo(true);
|
||||
|
||||
when(fingerprintRepository.isParentalConsentRequired(mApplication)).thenReturn(false);
|
||||
assertThat(viewModel.isParentalConsentRequired()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsBiometricUnlockDisabledByAdmin() {
|
||||
// We shall not mock FingerprintRepository, but
|
||||
// FingerprintRepository.isDisabledByAdmin() calls static method inside, we can't mock
|
||||
// static method
|
||||
final FingerprintRepository fingerprintRepository = mock(FingerprintRepository.class);
|
||||
final FingerprintEnrollIntroViewModel viewModel = new FingerprintEnrollIntroViewModel(
|
||||
mApplication, fingerprintRepository, newAllFalseRequest(mApplication),
|
||||
TEST_USER_ID);
|
||||
|
||||
when(fingerprintRepository.isDisabledByAdmin(mApplication, TEST_USER_ID)).thenReturn(true);
|
||||
assertThat(viewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(true);
|
||||
|
||||
when(fingerprintRepository.isDisabledByAdmin(mApplication, TEST_USER_ID)).thenReturn(false);
|
||||
assertThat(viewModel.isBiometricUnlockDisabledByAdmin()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetHasScrolledToBottom() {
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(mApplication));
|
||||
|
||||
viewModel.setHasScrolledToBottom(true);
|
||||
FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.hasScrollToBottom()).isEqualTo(true);
|
||||
|
||||
viewModel.setHasScrolledToBottom(false);
|
||||
status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.hasScrollToBottom()).isEqualTo(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnNextButtonClick_enrollNext() {
|
||||
// Set latest status to FINGERPRINT_ENROLLABLE_OK
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 0);
|
||||
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
|
||||
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(mApplication));
|
||||
FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK);
|
||||
|
||||
// Perform click on `next`
|
||||
viewModel.onNextButtonClick();
|
||||
|
||||
assertThat(viewModel.getActionLiveData().getValue())
|
||||
.isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnNextButtonClick_doneAndFinish() {
|
||||
// Set latest status to FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX
|
||||
setupFingerprintEnrolledFingerprints(mFingerprintManager, TEST_USER_ID, 1);
|
||||
setupSuwMaxFingerprintsEnrollable(mApplication, mResources, 1);
|
||||
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newIsSuwRequest(mApplication));
|
||||
FingerprintEnrollIntroStatus status = viewModel.getPageStatusLiveData().getValue();
|
||||
assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX);
|
||||
|
||||
// Perform click on `next`
|
||||
viewModel.onNextButtonClick();
|
||||
|
||||
assertThat(viewModel.getActionLiveData().getValue())
|
||||
.isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOnSkipOrCancelButtonClick() {
|
||||
final FingerprintEnrollIntroViewModel viewModel = newFingerprintEnrollIntroViewModel(
|
||||
newFingerprintRepository(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5),
|
||||
newAllFalseRequest(mApplication));
|
||||
|
||||
viewModel.onSkipOrCancelButtonClick();
|
||||
|
||||
assertThat(viewModel.getActionLiveData().getValue())
|
||||
.isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL);
|
||||
}
|
||||
}
|
@@ -0,0 +1,377 @@
|
||||
/*
|
||||
* 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.ExperimentalCoroutinesApi
|
||||
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()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isNotSuw() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuwPortal() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(application))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() = runTest {
|
||||
testOnStartToUpdateEnrollableStatusReachMax(
|
||||
newIsSuwSuggestedActionFlowRequest(application)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@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)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun TestScope.listOfActionFlow(
|
||||
viewModel: FingerprintEnrollIntroViewModel
|
||||
): List<FingerprintEnrollIntroAction> =
|
||||
mutableListOf<FingerprintEnrollIntroAction>().also {
|
||||
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||
viewModel.actionFlow.toList(it)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
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
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user