Merge "[BiometricsV2] Refactor EnrollIntroViewModel" into main

This commit is contained in:
Treehugger Robot
2023-08-07 05:22:36 +00:00
committed by Android (Google) Code Review
6 changed files with 580 additions and 607 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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