From 5f48cbafdf0fc5b88b0c0a10463a1013872d92ca Mon Sep 17 00:00:00 2001 From: Hao Dong Date: Tue, 29 Aug 2023 22:36:45 +0000 Subject: [PATCH] Implement FingerprintEnrollFindSensorV2Fragment Bug: 295206773 Test: N/A Change-Id: I5ddbdc925d6aefd0994016449d5baa5feae3de6b --- .../fingerprint/FingerprintErrorDialog.java | 4 +- .../FingerprintEnrollmentV2Activity.kt | 60 +++++- .../FingerprintEnrollEnrollingV2Fragment.kt | 3 +- .../FingerprintEnrollFindSensorV2Fragment.kt | 181 +++++++++++++++++- .../FingerprintEnrollIntroV2Fragment.kt | 5 +- .../viewmodel/AccessibilityViewModel.kt | 56 ++++++ .../FingerprintEnrollFindSensorViewModel.kt | 173 +++++++++++++++++ .../viewmodel/FoldStateViewModel.kt | 58 ++++++ .../viewmodel/OrientationStateViewModel.kt | 82 ++++++++ 9 files changed, 609 insertions(+), 13 deletions(-) create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/AccessibilityViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FoldStateViewModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/OrientationStateViewModel.kt diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java b/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java index d65e057ab26..155ced519a3 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintErrorDialog.java @@ -30,10 +30,10 @@ import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; import com.android.settings.R; -import com.android.settings.biometrics.BiometricEnrollBase; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; /** Fingerprint error dialog, will be shown when an error occurs during fingerprint enrollment. */ @@ -95,7 +95,7 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment { return dialog; } - public static void showErrorDialog(BiometricEnrollBase host, int errMsgId, boolean isSetup) { + public static void showErrorDialog(FragmentActivity host, int errMsgId, boolean isSetup) { if (host.isFinishing()) { return; } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt index d43aebac239..31afcb7e05a 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/activity/FingerprintEnrollmentV2Activity.kt @@ -20,11 +20,13 @@ import android.annotation.ColorInt import android.app.Activity import android.content.Intent import android.content.res.ColorStateList +import android.content.res.Configuration import android.graphics.Color import android.hardware.fingerprint.FingerprintManager import android.os.Bundle import android.provider.Settings import android.util.Log +import android.view.accessibility.AccessibilityManager import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity @@ -44,17 +46,21 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.Finge import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.AccessibilityViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Confirmation import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Enrollment +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintGatekeeperViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Finish +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FoldStateViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Intro import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.LaunchConfirmDeviceCredential +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.OrientationStateViewModel import com.android.settings.password.ChooseLockGeneric import com.android.settings.password.ChooseLockSettingsHelper import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE @@ -72,6 +78,10 @@ private const val TAG = "FingerprintEnrollmentV2Activity" class FingerprintEnrollmentV2Activity : FragmentActivity() { private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel + private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel + private lateinit var accessibilityViewModel: AccessibilityViewModel + private lateinit var foldStateViewModel: FoldStateViewModel + private lateinit var orientationStateViewModel: OrientationStateViewModel private val coroutineDispatcher = Dispatchers.Default /** Result listener for ChooseLock activity flow. */ @@ -94,6 +104,11 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { super.onAttachedToWindow() } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + foldStateViewModel.onConfigurationChange(newConfig) + } + @ColorInt private fun getBackgroundColor(): Int { val stateList: ColorStateList? = @@ -178,16 +193,53 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() { ) )[FingerprintEnrollNavigationViewModel::class.java] + // Initialize FoldStateViewModel + foldStateViewModel = + ViewModelProvider(this, FoldStateViewModel.FoldStateViewModelFactory(context))[ + FoldStateViewModel::class.java] + foldStateViewModel.onConfigurationChange(resources.configuration) + // Initialize FingerprintViewModel - ViewModelProvider( - this, - FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor, backgroundDispatcher) - )[FingerprintEnrollViewModel::class.java] + fingerprintEnrollViewModel = + ViewModelProvider( + this, + FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory( + interactor, + backgroundDispatcher + ) + )[FingerprintEnrollViewModel::class.java] // Initialize scroll view model ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[ FingerprintScrollViewModel::class.java] + // Initialize AccessibilityViewModel + accessibilityViewModel = + ViewModelProvider( + this, + AccessibilityViewModel.AccessibilityViewModelFactory( + getSystemService(AccessibilityManager::class.java)!! + ) + )[AccessibilityViewModel::class.java] + + // Initialize OrientationViewModel + orientationStateViewModel = + ViewModelProvider(this, OrientationStateViewModel.OrientationViewModelFactory(context))[ + OrientationStateViewModel::class.java] + + // Initialize FingerprintEnrollFindSensorViewModel + ViewModelProvider( + this, + FingerprintEnrollFindSensorViewModel.FingerprintEnrollFindSensorViewModelFactory( + navigationViewModel, + fingerprintEnrollViewModel, + gatekeeperViewModel, + accessibilityViewModel, + foldStateViewModel, + orientationStateViewModel + ) + )[FingerprintEnrollFindSensorViewModel::class.java] + lifecycleScope.launch { navigationViewModel.navigationViewModel.filterNotNull().collect { Log.d(TAG, "navigationStep $it") diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt index 3f615ceb24b..0140d57a2e1 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollEnrollingV2Fragment.kt @@ -19,10 +19,11 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import com.android.settings.R import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel /** A fragment that is responsible for enrolling a users fingerprint. */ -class FingerprintEnrollEnrollingV2Fragment : Fragment() { +class FingerprintEnrollEnrollingV2Fragment : Fragment(R.layout.fingerprint_enroll_enrolling) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt index beb84e966ae..dcdcccfd7b4 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollFindSensorV2Fragment.kt @@ -17,26 +17,197 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.Surface +import android.view.View +import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import com.airbnb.lottie.LottieAnimationView import com.android.settings.R -import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel +import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog +import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation +import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.google.android.setupcompat.template.FooterBarMixin +import com.google.android.setupcompat.template.FooterButton +import com.google.android.setupdesign.GlifLayout +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +private const val TAG = "FingerprintEnrollFindSensorV2Fragment" /** * A fragment that is used to educate the user about the fingerprint sensor on this device. * + * If the sensor is not a udfps sensor, this fragment listens to fingerprint enrollment for + * proceeding to the enroll enrolling. + * * The main goals of this page are * 1. Inform the user where the fingerprint sensor is on their device * 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment] * will work. */ -class FingerprintEnrollFindSensorV2Fragment : Fragment(R.layout.fingerprint_v2_enroll_find_sensor) { +class FingerprintEnrollFindSensorV2Fragment : Fragment() { + // This is only for non-udfps or non-sfps sensor. For udfps and sfps, we show lottie. + private var animation: FingerprintFindSensorAnimation? = null + + private var contentLayoutId: Int = -1 + private lateinit var viewModel: FingerprintEnrollFindSensorViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (savedInstanceState == null) { - val navigationViewModel = - ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::class.java] + viewModel = + ViewModelProvider(requireActivity())[FingerprintEnrollFindSensorViewModel::class.java] + lifecycleScope.launch { + viewModel.sensorType.collect { + contentLayoutId = + when (it) { + FingerprintSensorType.UDFPS_OPTICAL, + FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout + FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout + else -> R.layout.fingerprint_v2_enroll_find_sensor + } + } } } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(contentLayoutId, container, false).also { it -> + val view = it!! as GlifLayout + + // Set up header and description + lifecycleScope.launch { viewModel.sensorType.collect { setTexts(it, view) } } + + // Set up footer bar + val footerBarMixin = view.getMixin(FooterBarMixin::class.java) + setupSecondaryButton(footerBarMixin) + lifecycleScope.launch { + viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) } + } + + // Set up lottie or animation + lifecycleScope.launch { + viewModel.showSfpsLottie.collect { (isFolded, rotation) -> + setupLottie(view, getSfpsIllustrationLottieAnimation(isFolded, rotation)) + } + } + lifecycleScope.launch { + viewModel.showUdfpsLottie.collect { isAccessibilityEnabled -> + val lottieAnimation = + if (isAccessibilityEnabled) R.raw.udfps_edu_a11y_lottie else R.raw.udfps_edu_lottie + setupLottie(view, lottieAnimation) { viewModel.proceedToEnrolling() } + } + } + lifecycleScope.launch { + viewModel.showRfpsAnimation.collect { + animation = view.findViewById(R.id.fingerprint_sensor_location_animation) + animation!!.startAnimation() + } + } + + lifecycleScope.launch { + viewModel.showErrorDialog.collect { (errMsgId, isSetup) -> + // TODO: Covert error dialog kotlin as well + FingerprintErrorDialog.showErrorDialog(requireActivity(), errMsgId, isSetup) + } + } + } + } + + override fun onDestroy() { + animation?.stopAnimation() + super.onDestroy() + } + + private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) { + footerBarMixin.secondaryButton = + FooterButton.Builder(requireActivity()) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setListener { + run { + // TODO: Show the dialog for suw + Log.d(TAG, "onSkipClicked") + // TODO: Finish activity in the root activity instead. + requireActivity().finish() + } + } + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary) + .build() + } + + private fun setupPrimaryButton(footerBarMixin: FooterBarMixin) { + footerBarMixin.primaryButton = + FooterButton.Builder(requireActivity()) + .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button) + .setListener { + run { + Log.d(TAG, "onStartButtonClick") + viewModel.proceedToEnrolling() + } + } + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary) + .build() + } + + private fun setupLottie( + view: View, + lottieAnimation: Int, + lottieClickListener: View.OnClickListener? = null + ) { + val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie) + illustrationLottie?.setAnimation(lottieAnimation) + illustrationLottie?.playAnimation() + illustrationLottie?.setOnClickListener(lottieClickListener) + illustrationLottie?.visibility = View.VISIBLE + } + + private fun setTexts(sensorType: FingerprintSensorType, view: GlifLayout) { + when (sensorType) { + FingerprintSensorType.UDFPS_OPTICAL, + FingerprintSensorType.UDFPS_ULTRASONIC -> { + view.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title) + view.setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message) + } + FingerprintSensorType.POWER_BUTTON -> { + view.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title) + view.setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message) + } + else -> { + view.setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title) + view.setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message) + } + } + } + + private fun getSfpsIllustrationLottieAnimation(isFolded: Boolean, rotation: Int): Int { + val animation: Int + when (rotation) { + Surface.ROTATION_90 -> + animation = + (if (isFolded) R.raw.fingerprint_edu_lottie_folded_top_left + else R.raw.fingerprint_edu_lottie_portrait_top_left) + Surface.ROTATION_180 -> + animation = + (if (isFolded) R.raw.fingerprint_edu_lottie_folded_bottom_left + else R.raw.fingerprint_edu_lottie_landscape_bottom_left) + Surface.ROTATION_270 -> + animation = + (if (isFolded) R.raw.fingerprint_edu_lottie_folded_bottom_right + else R.raw.fingerprint_edu_lottie_portrait_bottom_right) + else -> + animation = + (if (isFolded) R.raw.fingerprint_edu_lottie_folded_top_right + else R.raw.fingerprint_edu_lottie_landscape_top_right) + } + return animation + } } diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt index 03c7a5f46e7..dbf6d128c0d 100644 --- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/fragment/FingerprintEnrollIntroV2Fragment.kt @@ -180,7 +180,10 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll scrollView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES // Next button responsible for starting the next fragment. val onNextButtonClick: View.OnClickListener = - View.OnClickListener { Log.d(TAG, "OnNextClicked") } + View.OnClickListener { + Log.d(TAG, "OnNextClicked") + navigationViewModel.nextStep() + } val layout: GlifLayout = requireActivity().requireViewById(R.id.setup_wizard_layout) footerBarMixin = layout.getMixin(FooterBarMixin::class.java) diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/AccessibilityViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/AccessibilityViewModel.kt new file mode 100644 index 00000000000..a86ad5d9942 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/AccessibilityViewModel.kt @@ -0,0 +1,56 @@ +/* + * 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.biometrics.fingerprint2.ui.enrollment.viewmodel + +import android.view.accessibility.AccessibilityManager +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.stateIn + +/** Represents all of the information on accessibility state. */ +class AccessibilityViewModel(accessibilityManager: AccessibilityManager) : ViewModel() { + /** A flow that contains whether or not accessibility is enabled */ + val isAccessibilityEnabled: Flow = + callbackFlow { + val listener = + AccessibilityManager.AccessibilityStateChangeListener { enabled -> trySend(enabled) } + accessibilityManager.addAccessibilityStateChangeListener(listener) + + // This clause will be called when no one is listening to the flow + awaitClose { accessibilityManager.removeAccessibilityStateChangeListener(listener) } + } + .stateIn( + viewModelScope, // This is going to tied to the view model scope + SharingStarted.WhileSubscribed(), // When no longer subscribed, we removeTheListener + false + ) + + class AccessibilityViewModelFactory(private val accessibilityManager: AccessibilityManager) : + ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return AccessibilityViewModel(accessibilityManager) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt new file mode 100644 index 00000000000..dbf6b33de5a --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FingerprintEnrollFindSensorViewModel.kt @@ -0,0 +1,173 @@ +/* + * 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.biometrics.fingerprint2.ui.enrollment.viewmodel + +import android.hardware.fingerprint.FingerprintManager +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** Models the UI state for [FingerprintEnrollFindSensorV2Fragment]. */ +class FingerprintEnrollFindSensorViewModel( + private val navigationViewModel: FingerprintEnrollNavigationViewModel, + private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, + private val gatekeeperViewModel: FingerprintGatekeeperViewModel, + accessibilityViewModel: AccessibilityViewModel, + foldStateViewModel: FoldStateViewModel, + orientationStateViewModel: OrientationStateViewModel +) : ViewModel() { + /** Represents the stream of sensor type. */ + val sensorType: Flow = + fingerprintEnrollViewModel.sensorType.filterWhenEducationIsShown() + private val _isUdfps: Flow = + sensorType.map { + it == FingerprintSensorType.UDFPS_OPTICAL || it == FingerprintSensorType.UDFPS_ULTRASONIC + } + private val _isSfps: Flow = sensorType.map { it == FingerprintSensorType.POWER_BUTTON } + private val _isRearSfps: Flow = + combineTransform(_isSfps, _isUdfps) { v1, v2 -> !v1 && !v2 } + + /** Represents the stream of showing primary button. */ + val showPrimaryButton: Flow = _isUdfps.transform { if (it) emit(true) } + + /** Represents the stream of showing sfps lottie, Pair(isFolded, rotation). */ + val showSfpsLottie: Flow> = + combineTransform( + _isSfps, + foldStateViewModel.isFolded, + orientationStateViewModel.rotation, + ) { isSfps, isFolded, rotation -> + if (isSfps) emit(Pair(isFolded, rotation)) + } + + /** Represents the stream of showing udfps lottie. */ + val showUdfpsLottie: Flow = + combineTransform( + _isUdfps, + accessibilityViewModel.isAccessibilityEnabled, + ) { isUdfps, isAccessibilityEnabled -> + if (isUdfps) emit(isAccessibilityEnabled) + } + + /** Represents the stream of showing rfps animation. */ + val showRfpsAnimation: Flow = _isRearSfps.transform { if (it) emit(true) } + + private val _showErrorDialog: MutableStateFlow?> = MutableStateFlow(null) + /** Represents the stream of showing error dialog. */ + val showErrorDialog = _showErrorDialog.filterNotNull() + + init { + // Start or end enroll flow + viewModelScope.launch { + combine( + fingerprintEnrollViewModel.sensorType, + gatekeeperViewModel.hasValidGatekeeperInfo, + gatekeeperViewModel.gatekeeperInfo, + navigationViewModel.navigationViewModel + ) { sensorType, hasValidGatekeeperInfo, gatekeeperInfo, navigationViewModel -> + val shouldStartEnroll = + navigationViewModel.currStep == Education && + sensorType != FingerprintSensorType.UDFPS_OPTICAL && + sensorType != FingerprintSensorType.UDFPS_ULTRASONIC && + hasValidGatekeeperInfo + if (shouldStartEnroll) (gatekeeperInfo as GatekeeperInfo.GatekeeperPasswordInfo).token + else null + } + .collect { token -> + if (token != null) { + fingerprintEnrollViewModel.startEnroll(token, EnrollReason.FindSensor) + } else { + fingerprintEnrollViewModel.stopEnroll() + } + } + } + + // Enroll progress flow + viewModelScope.launch { + combine( + navigationViewModel.enrollType, + fingerprintEnrollViewModel.enrollFlow.filterNotNull() + ) { enrollType, enrollFlow -> + Pair(enrollType, enrollFlow) + } + .collect { (enrollType, enrollFlow) -> + when (enrollFlow) { + // TODO: Cancel the enroll() when EnrollProgress is received instead of proceeding to + // Enrolling page. Otherwise Enrolling page will receive the EnrollError. + is FingerEnrollStateViewModel.EnrollProgress -> proceedToEnrolling() + is FingerEnrollStateViewModel.EnrollError -> { + val errMsgId = enrollFlow.errMsgId + if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) { + proceedToEnrolling() + } else { + _showErrorDialog.update { Pair(errMsgId, enrollType == SetupWizard) } + } + } + is FingerEnrollStateViewModel.EnrollHelp -> {} + } + } + } + } + + /** Proceed to EnrollEnrolling page. */ + fun proceedToEnrolling() { + navigationViewModel.nextStep() + } + + // TODO: If we decide to remove previous fragment from activity, then we don't need to check + // whether education is shown for the flows that are subscribed by + // [FingerprintEnrollFindSensorV2Fragment]. + private fun Flow.filterWhenEducationIsShown() = + combineTransform(navigationViewModel.navigationViewModel) { value, navigationViewModel -> + if (navigationViewModel.currStep == Education) { + emit(value) + } + } + + class FingerprintEnrollFindSensorViewModelFactory( + private val navigationViewModel: FingerprintEnrollNavigationViewModel, + private val fingerprintEnrollViewModel: FingerprintEnrollViewModel, + private val gatekeeperViewModel: FingerprintGatekeeperViewModel, + private val accessibilityViewModel: AccessibilityViewModel, + private val foldStateViewModel: FoldStateViewModel, + private val orientationStateViewModel: OrientationStateViewModel + ) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FingerprintEnrollFindSensorViewModel( + navigationViewModel, + fingerprintEnrollViewModel, + gatekeeperViewModel, + accessibilityViewModel, + foldStateViewModel, + orientationStateViewModel + ) + as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FoldStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FoldStateViewModel.kt new file mode 100644 index 00000000000..a4c7ff22c57 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/FoldStateViewModel.kt @@ -0,0 +1,58 @@ +/* + * 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.biometrics.fingerprint2.ui.enrollment.viewmodel + +import android.content.Context +import android.content.res.Configuration +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider +import com.android.systemui.unfold.updates.FoldProvider +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +/** Represents all of the information on fold state. */ +class FoldStateViewModel(context: Context) : ViewModel() { + + private val screenSizeFoldProvider = ScreenSizeFoldProvider(context) + + /** A flow that contains the fold state info */ + val isFolded: Flow = callbackFlow { + val foldStateListener = + object : FoldProvider.FoldCallback { + override fun onFoldUpdated(isFolded: Boolean) { + trySend(isFolded) + } + } + screenSizeFoldProvider.registerCallback(foldStateListener, context.mainExecutor) + awaitClose { screenSizeFoldProvider.unregisterCallback(foldStateListener) } + } + + fun onConfigurationChange(newConfig: Configuration) { + screenSizeFoldProvider.onConfigurationChange(newConfig) + } + + class FoldStateViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return FoldStateViewModel(context) as T + } + } +} diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/OrientationStateViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/OrientationStateViewModel.kt new file mode 100644 index 00000000000..2e5f7341743 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/viewmodel/OrientationStateViewModel.kt @@ -0,0 +1,82 @@ +/* + * 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.biometrics.fingerprint2.ui.enrollment.viewmodel + +import android.content.Context +import android.view.OrientationEventListener +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.android.internal.R +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.stateIn + +/** Represents all of the information on orientation state and rotation state. */ +class OrientationStateViewModel(private val context: Context) : ViewModel() { + + /** A flow that contains the orientation info */ + val orientation: Flow = callbackFlow { + val orientationEventListener = + object : OrientationEventListener(context) { + override fun onOrientationChanged(orientation: Int) { + trySend(orientation) + } + } + orientationEventListener.enable() + awaitClose { orientationEventListener.disable() } + } + + /** A flow that contains the rotation info */ + val rotation: Flow = + callbackFlow { + val orientationEventListener = + object : OrientationEventListener(context) { + override fun onOrientationChanged(orientation: Int) { + trySend(getRotationFromDefault(context.display!!.rotation)) + } + } + orientationEventListener.enable() + awaitClose { orientationEventListener.disable() } + } + .stateIn( + viewModelScope, // This is going to tied to the view model scope + SharingStarted.WhileSubscribed(), // When no longer subscribed, we removeTheListener + context.display!!.rotation + ) + + fun getRotationFromDefault(rotation: Int): Int { + val isReverseDefaultRotation = + context.resources.getBoolean(R.bool.config_reverseDefaultRotation) + return if (isReverseDefaultRotation) { + (rotation + 1) % 4 + } else { + rotation + } + } + + class OrientationViewModelFactory(private val context: Context) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + ): T { + return OrientationStateViewModel(context) as T + } + } +}