Merge "Implement FingerprintEnrollFindSensorV2Fragment" into main

This commit is contained in:
Hao Dong
2023-09-22 00:00:07 +00:00
committed by Android (Google) Code Review
9 changed files with 609 additions and 13 deletions

View File

@@ -30,10 +30,10 @@ import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.biometrics.BiometricEnrollBase;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/** Fingerprint error dialog, will be shown when an error occurs during fingerprint enrollment. */ /** Fingerprint error dialog, will be shown when an error occurs during fingerprint enrollment. */
@@ -95,7 +95,7 @@ public class FingerprintErrorDialog extends InstrumentedDialogFragment {
return dialog; 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()) { if (host.isFinishing()) {
return; return;
} }

View File

@@ -20,11 +20,13 @@ import android.annotation.ColorInt
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintManager
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.accessibility.AccessibilityManager
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity 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.FingerprintEnrollEnrollingV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment 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.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.Confirmation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Education 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.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.FingerprintEnrollNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollViewModel 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.FingerprintGatekeeperViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel 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.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.GatekeeperInfo
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Intro 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.LaunchConfirmDeviceCredential
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.OrientationStateViewModel
import com.android.settings.password.ChooseLockGeneric import com.android.settings.password.ChooseLockGeneric
import com.android.settings.password.ChooseLockSettingsHelper import com.android.settings.password.ChooseLockSettingsHelper
import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
@@ -72,6 +78,10 @@ private const val TAG = "FingerprintEnrollmentV2Activity"
class FingerprintEnrollmentV2Activity : FragmentActivity() { class FingerprintEnrollmentV2Activity : FragmentActivity() {
private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel private lateinit var navigationViewModel: FingerprintEnrollNavigationViewModel
private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel 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 private val coroutineDispatcher = Dispatchers.Default
/** Result listener for ChooseLock activity flow. */ /** Result listener for ChooseLock activity flow. */
@@ -94,6 +104,11 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
super.onAttachedToWindow() super.onAttachedToWindow()
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
foldStateViewModel.onConfigurationChange(newConfig)
}
@ColorInt @ColorInt
private fun getBackgroundColor(): Int { private fun getBackgroundColor(): Int {
val stateList: ColorStateList? = val stateList: ColorStateList? =
@@ -178,16 +193,53 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
) )
)[FingerprintEnrollNavigationViewModel::class.java] )[FingerprintEnrollNavigationViewModel::class.java]
// Initialize FoldStateViewModel
foldStateViewModel =
ViewModelProvider(this, FoldStateViewModel.FoldStateViewModelFactory(context))[
FoldStateViewModel::class.java]
foldStateViewModel.onConfigurationChange(resources.configuration)
// Initialize FingerprintViewModel // Initialize FingerprintViewModel
ViewModelProvider( fingerprintEnrollViewModel =
this, ViewModelProvider(
FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(interactor, backgroundDispatcher) this,
)[FingerprintEnrollViewModel::class.java] FingerprintEnrollViewModel.FingerprintEnrollViewModelFactory(
interactor,
backgroundDispatcher
)
)[FingerprintEnrollViewModel::class.java]
// Initialize scroll view model // Initialize scroll view model
ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[ ViewModelProvider(this, FingerprintScrollViewModel.FingerprintScrollViewModelFactory())[
FingerprintScrollViewModel::class.java] 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 { lifecycleScope.launch {
navigationViewModel.navigationViewModel.filterNotNull().collect { navigationViewModel.navigationViewModel.filterNotNull().collect {
Log.d(TAG, "navigationStep $it") Log.d(TAG, "navigationStep $it")

View File

@@ -19,10 +19,11 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollNavigationViewModel
/** A fragment that is responsible for enrolling a users fingerprint. */ /** 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -17,26 +17,197 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.airbnb.lottie.LottieAnimationView
import com.android.settings.R 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. * 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 * The main goals of this page are
* 1. Inform the user where the fingerprint sensor is on their device * 1. Inform the user where the fingerprint sensor is on their device
* 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment] * 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
* will work. * 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (savedInstanceState == null) { viewModel =
val navigationViewModel = ViewModelProvider(requireActivity())[FingerprintEnrollFindSensorViewModel::class.java]
ViewModelProvider(requireActivity())[FingerprintEnrollNavigationViewModel::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
}
} }

View File

@@ -180,7 +180,10 @@ class FingerprintEnrollIntroV2Fragment : Fragment(R.layout.fingerprint_v2_enroll
scrollView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES scrollView.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
// Next button responsible for starting the next fragment. // Next button responsible for starting the next fragment.
val onNextButtonClick: View.OnClickListener = 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) val layout: GlifLayout = requireActivity().requireViewById(R.id.setup_wizard_layout)
footerBarMixin = layout.getMixin(FooterBarMixin::class.java) footerBarMixin = layout.getMixin(FooterBarMixin::class.java)

View File

@@ -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<Boolean> =
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 <T : ViewModel> create(
modelClass: Class<T>,
): T {
return AccessibilityViewModel(accessibilityManager) as T
}
}
}

View File

@@ -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<FingerprintSensorType> =
fingerprintEnrollViewModel.sensorType.filterWhenEducationIsShown()
private val _isUdfps: Flow<Boolean> =
sensorType.map {
it == FingerprintSensorType.UDFPS_OPTICAL || it == FingerprintSensorType.UDFPS_ULTRASONIC
}
private val _isSfps: Flow<Boolean> = sensorType.map { it == FingerprintSensorType.POWER_BUTTON }
private val _isRearSfps: Flow<Boolean> =
combineTransform(_isSfps, _isUdfps) { v1, v2 -> !v1 && !v2 }
/** Represents the stream of showing primary button. */
val showPrimaryButton: Flow<Boolean> = _isUdfps.transform { if (it) emit(true) }
/** Represents the stream of showing sfps lottie, Pair(isFolded, rotation). */
val showSfpsLottie: Flow<Pair<Boolean, Int>> =
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<Boolean> =
combineTransform(
_isUdfps,
accessibilityViewModel.isAccessibilityEnabled,
) { isUdfps, isAccessibilityEnabled ->
if (isUdfps) emit(isAccessibilityEnabled)
}
/** Represents the stream of showing rfps animation. */
val showRfpsAnimation: Flow<Boolean> = _isRearSfps.transform { if (it) emit(true) }
private val _showErrorDialog: MutableStateFlow<Pair<Int, Boolean>?> = 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 <T> Flow<T>.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 <T : ViewModel> create(modelClass: Class<T>): T {
return FingerprintEnrollFindSensorViewModel(
navigationViewModel,
fingerprintEnrollViewModel,
gatekeeperViewModel,
accessibilityViewModel,
foldStateViewModel,
orientationStateViewModel
)
as T
}
}
}

View File

@@ -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<Boolean> = 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 <T : ViewModel> create(
modelClass: Class<T>,
): T {
return FoldStateViewModel(context) as T
}
}
}

View File

@@ -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<Int> = 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<Int> =
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 <T : ViewModel> create(
modelClass: Class<T>,
): T {
return OrientationStateViewModel(context) as T
}
}
}