diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt index 2ba1df19f0a..e7e1cc8fc57 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.kt @@ -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 = - 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(R.id.sud_scroll_view)?.importantForAccessibility = + View.IMPORTANT_FOR_ACCESSIBILITY_YES } diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt index 562b7dd317b..c295bb35ce9 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.kt @@ -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 = Observer { action -> - if (DEBUG) { - Log.d(TAG, "introActionObserver($action)") - } - action?.let { onIntroAction(it) } - } - private val findSensorActionObserver: Observer = Observer { 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() } } diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java deleted file mode 100644 index 5e9085ab4a6..00000000000 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java +++ /dev/null @@ -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 mHasScrolledToBottomLiveData = - new MutableLiveData<>(HAS_SCROLLED_TO_BOTTOM_DEFAULT); - private final MutableLiveData mEnrollableStatusLiveData = - new MutableLiveData<>(ENROLLABLE_STATUS_DEFAULT); - private final MediatorLiveData mPageStatusLiveData = - new MediatorLiveData<>(); - private final MutableLiveData 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 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 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); - } -} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.kt b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.kt new file mode 100644 index 00000000000..98137b4ed22 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.kt @@ -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() + val actionFlow: SharedFlow + get() = _actionFlow.asSharedFlow() + + private fun getEnrollableStatus(): FingerprintEnrollable { + val num = fingerprintRepository.getNumOfEnrolledFingerprintsSize(userId) + val max = + if (request.isSuw && !request.isAfterSuwOrSuwSuggestedAction) + fingerprintRepository.getMaxFingerprintsInSuw( + getApplication().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 = + 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 +} diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java deleted file mode 100644 index 12b860bf34d..00000000000 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java +++ /dev/null @@ -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 actionLiveData = - (MutableLiveData) 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); - } -} diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.kt b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.kt new file mode 100644 index 00000000000..08e5ac35d7f --- /dev/null +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.kt @@ -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 = + mutableListOf().also { + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.actionFlow.toList(it) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun TestScope.listOfPageStatusFlow( + viewModel: FingerprintEnrollIntroViewModel + ): List = + mutableListOf().also { + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + viewModel.pageStatusFlow.toList(it) + } + } + + companion object { + private const val TEST_USER_ID = 33 + } +}