diff --git a/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml b/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
index 32df66592f3..ddd2c30d807 100644
--- a/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
+++ b/res/layout/fingerprint_v2_udfps_enroll_enrolling.xml
@@ -1,6 +1,6 @@
-
+ android:orientation="vertical">
-
+
+
+
+
+
+
+
+
+
+
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ android:scaleType="centerInside"
+ app:lottie_autoPlay="true"
+ app:lottie_loop="true"
+ app:lottie_speed=".85" />
-
+
-
-
-
-
-
-
-
-
+
+
-
+
diff --git a/res/layout/fingerprint_v2_udfps_enroll_view.xml b/res/layout/fingerprint_v2_udfps_enroll_view.xml
new file mode 100644
index 00000000000..20df6e138ea
--- /dev/null
+++ b/res/layout/fingerprint_v2_udfps_enroll_view.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt
index 000a4774a78..b7616e4c4b3 100644
--- a/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/data/repository/FingerprintSensorRepository.kt
@@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.withContext
/**
@@ -45,12 +46,12 @@ interface FingerprintSensorRepository {
}
class FingerprintSensorRepositoryImpl(
- fingerprintManager: FingerprintManager,
- backgroundDispatcher: CoroutineDispatcher,
- activityScope: CoroutineScope,
+ fingerprintManager: FingerprintManager,
+ backgroundDispatcher: CoroutineDispatcher,
+ activityScope: CoroutineScope,
) : FingerprintSensorRepository {
- override val fingerprintSensor: Flow =
+ private val fingerprintPropsInternal: Flow =
callbackFlow {
val callback =
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -60,7 +61,7 @@ class FingerprintSensorRepositoryImpl(
if (sensors.isEmpty()) {
trySend(DEFAULT_PROPS)
} else {
- trySend(sensors[0].toFingerprintSensor())
+ trySend(sensors[0])
}
}
}
@@ -71,20 +72,24 @@ class FingerprintSensorRepositoryImpl(
}
.stateIn(activityScope, started = SharingStarted.Eagerly, initialValue = DEFAULT_PROPS)
- companion object {
- private const val TAG = "FingerprintSensorRepoImpl"
-
- private val DEFAULT_PROPS =
- FingerprintSensorPropertiesInternal(
- -1 /* sensorId */,
- SensorProperties.STRENGTH_CONVENIENCE,
- 0 /* maxEnrollmentsPerUser */,
- listOf(),
- FingerprintSensorProperties.TYPE_UNKNOWN,
- false /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- listOf(SensorLocationInternal.DEFAULT),
- )
- .toFingerprintSensor()
+ override val fingerprintSensor: Flow =
+ fingerprintPropsInternal.transform {
+ emit(it.toFingerprintSensor())
}
+
+ companion object {
+ private const val TAG = "FingerprintSensorRepoImpl"
+
+ private val DEFAULT_PROPS =
+ FingerprintSensorPropertiesInternal(
+ -1 /* sensorId */,
+ SensorProperties.STRENGTH_CONVENIENCE,
+ 0 /* maxEnrollmentsPerUser */,
+ listOf(),
+ FingerprintSensorProperties.TYPE_UNKNOWN,
+ false /* halControlsIllumination */,
+ true /* resetLockoutRequiresHardwareAuthToken */,
+ listOf(SensorLocationInternal.DEFAULT),
+ )
+ }
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
index 900f7cfb734..652bc0c312f 100644
--- a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractorImpl.kt
@@ -20,10 +20,12 @@ import android.content.Context
import android.content.Intent
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFingerprintConstants
+import android.hardware.biometrics.SensorLocationInternal
import android.hardware.fingerprint.FingerprintEnrollOptions;
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback
import android.hardware.fingerprint.FingerprintManager.RemovalCallback
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.CancellationSignal
import android.util.Log
import com.android.settings.biometrics.GatekeeperPasswordProvider
diff --git a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt
index c0e1b4aeaf4..105929dd33b 100644
--- a/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/lib/domain/interactor/FingerprintManagerInteractor.kt
@@ -16,6 +16,7 @@
package com.android.settings.biometrics.fingerprint2.lib.domain.interactor
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
index a9cd16fef1e..f6917f3bf0e 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/fragment/RFPSEnrollFragment.kt
@@ -33,7 +33,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.settings.R
-import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSIconTouchViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
@@ -42,7 +41,6 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enroll
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
-import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
import com.android.settings.core.instrumentation.InstrumentedDialogFragment
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
@@ -86,6 +84,7 @@ class RFPSEnrollFragment() : Fragment(R.layout.fingerprint_v2_rfps_enroll_enroll
private val backgroundViewModel: BackgroundViewModel by lazy {
viewModelProvider[BackgroundViewModel::class.java]
}
+
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt
index 5a6fc149b3e..dd7d9f53b3a 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/rfps/ui/widget/RFPSProgressBar.kt
@@ -24,7 +24,6 @@ import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.util.AttributeSet
-import android.util.Log
import android.view.animation.AnimationUtils
import android.view.animation.Interpolator
import com.android.settings.R
@@ -82,7 +81,6 @@ class RFPSProgressBar : RingProgressBar {
}
shouldAnimateInternal = shouldAnimate
-
}
/** This function should only be called when actual progress has been made. */
fun updateProgress(percentComplete: Float) {
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
index 61451287dc6..9dfc222b095 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/fragment/UdfpsEnrollFragment.kt
@@ -17,8 +17,9 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.fragment
import android.os.Bundle
-import android.util.Log
import android.view.View
+import android.view.WindowManager
+import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
@@ -28,89 +29,23 @@ import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.DescriptionText
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.HeaderText
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.EducationAnimationModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
import com.google.android.setupdesign.GlifLayout
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+/** This fragment is responsible for showing the udfps Enrollment UI. */
class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enrolling) {
/** Used for testing purposes */
private var factory: ViewModelProvider.Factory? = null
private val viewModel: UdfpsViewModel by lazy { viewModelProvider[UdfpsViewModel::class.java] }
-
- @VisibleForTesting
- constructor(theFactory: ViewModelProvider.Factory) : this() {
- factory = theFactory
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- val layout = view as GlifLayout
- val illustrationLottie: LottieAnimationView = layout.findViewById(R.id.illustration_lottie)!!
-
- viewLifecycleOwner.lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.RESUMED) {
- viewLifecycleOwner.lifecycleScope.launch {
- viewModel.stageFlow.collect {
- layout.setHeaderText(getHeaderText(it))
- getDescriptionText(it)?.let { descriptionText ->
- layout.setDescriptionText(descriptionText)
- }
- getLottie(it)?.let { lottie ->
- layout.descriptionText = ""
- LottieCompositionFactory.fromRawRes(requireContext().applicationContext, lottie)
- .addListener { comp ->
- comp?.let { composition ->
- viewLifecycleOwner.lifecycleScope.launch {
- illustrationLottie.setComposition(composition)
- illustrationLottie.visibility = View.VISIBLE
- illustrationLottie.playAnimation()
- }
- }
- }
- }
- }
- }
- }
- }
- }
-
- private fun getHeaderText(stageViewModel: StageViewModel): Int {
- return when (stageViewModel) {
- StageViewModel.Center,
- StageViewModel.Guided,
- StageViewModel.Fingertip,
- StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title
- StageViewModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title
- StageViewModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title
- }
- }
-
- private fun getDescriptionText(stageViewModel: StageViewModel): Int? {
- return when (stageViewModel) {
- StageViewModel.Center,
- StageViewModel.Guided,
- StageViewModel.Fingertip,
- StageViewModel.LeftEdge,
- StageViewModel.RightEdge -> null
- StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_start_message
- }
- }
-
- private fun getLottie(stageViewModel: StageViewModel): Int? {
- return when (stageViewModel) {
- StageViewModel.Center,
- StageViewModel.Guided -> R.raw.udfps_center_hint_lottie
- StageViewModel.Fingertip -> R.raw.udfps_tip_hint_lottie
- StageViewModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie
- StageViewModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie
- StageViewModel.Unknown -> null
- }
- }
+ private lateinit var udfpsEnrollView: UdfpsEnrollViewV2
private val viewModelProvider: ViewModelProvider by lazy {
if (factory != null) {
@@ -120,6 +55,111 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
}
+ @VisibleForTesting
+ constructor(theFactory: ViewModelProvider.Factory) : this() {
+ factory = theFactory
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ val illustrationLottie: LottieAnimationView = view.findViewById(R.id.illustration_lottie)!!
+ udfpsEnrollView = view.findViewById(R.id.udfps_animation_view)!!
+ val titleTextView = view.findViewById(R.id.title)!!
+ val descriptionTextView = view.findViewById(R.id.description)!!
+
+ val glifLayout = view.findViewById(R.id.dummy_glif_layout)!!
+ val backgroundColor = glifLayout.backgroundBaseColor
+ val window = requireActivity().window
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
+ val color = backgroundColor?.defaultColor ?: glifLayout.primaryColor.defaultColor
+ window.statusBarColor = color
+ view.setBackgroundColor(color)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.headerText.collect { titleTextView.setText(it.toResource()) }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.descriptionText.collect {
+ if (it != null) {
+ it.toResource()?.let { text -> descriptionTextView.setText(text) }
+ } else {
+ descriptionTextView.text = ""
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.sensorLocation.collect { rect -> udfpsEnrollView.setSensorRect(rect) }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.accessibilityEnabled.collect { isEnabled -> udfpsEnrollView.setAccessibilityEnabled(isEnabled) }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.lottie.collect { lottieModel ->
+ val resource = lottieModel.toResource()
+ if (resource != null) {
+ LottieCompositionFactory.fromRawRes(requireContext(), resource).addListener { comp ->
+ comp?.let { composition ->
+ illustrationLottie.setComposition(composition)
+ illustrationLottie.visibility = View.VISIBLE
+ illustrationLottie.playAnimation()
+ }
+ }
+ } else {
+ illustrationLottie.visibility = View.INVISIBLE
+ }
+ }
+ }
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.udfpsEvent.collect { udfpsEnrollView.onUdfpsEvent(it) }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.enrollStage.collect { udfpsEnrollView.updateStage(it) }
+ }
+ }
+ }
+ }
+
+ private fun HeaderText.toResource(): Int {
+ return when (this.stageViewModel) {
+ StageViewModel.Center,
+ StageViewModel.Guided,
+ StageViewModel.Fingertip,
+ StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title
+ StageViewModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title
+ StageViewModel.RightEdge -> R.string.security_settings_udfps_enroll_right_edge_title
+ }
+ }
+
+ private fun DescriptionText.toResource(): Int? {
+ return when (this.stageViewModel) {
+ StageViewModel.Center,
+ StageViewModel.Guided,
+ StageViewModel.Fingertip,
+ StageViewModel.LeftEdge,
+ StageViewModel.RightEdge -> null
+ StageViewModel.Unknown -> R.string.security_settings_udfps_enroll_start_message
+ }
+ }
+
+ private fun EducationAnimationModel.toResource(): Int? {
+ return when (this.stageViewModel) {
+ StageViewModel.Center,
+ StageViewModel.Guided -> R.raw.udfps_center_hint_lottie
+ StageViewModel.Fingertip -> R.raw.udfps_tip_hint_lottie
+ StageViewModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie
+ StageViewModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie
+ StageViewModel.Unknown -> null
+ }
+ }
+
companion object {
private const val TAG = "UDFPSEnrollFragment"
private val navStep = FingerprintNavigationStep.Enrollment::class
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt
new file mode 100644
index 00000000000..192a7871e78
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.viewmodel
+
+/** Represents the description text for UDFPS enrollment */
+data class DescriptionText(
+ val isSuw: Boolean,
+ val isAccessibility: Boolean,
+ val stageViewModel: StageViewModel,
+)
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt
new file mode 100644
index 00000000000..cf125c37411
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.viewmodel
+
+/** Represents the lottie for UDFPS enrollment */
+data class EducationAnimationModel(
+ val isSuw: Boolean,
+ val isAccessibility: Boolean,
+ val stageViewModel: StageViewModel,
+)
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt
new file mode 100644
index 00000000000..9cfcddc8d4d
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.viewmodel
+
+/** Represents the header text for UDFPS enrollment */
+data class HeaderText(
+ val isSuw: Boolean,
+ val isAccessibility: Boolean,
+ val stageViewModel: StageViewModel,
+)
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt
index b879ce17e7b..75eaec786b4 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/StageViewModel.kt
@@ -22,15 +22,30 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrol
* of the where the user should press their fingerprint
*/
sealed class StageViewModel {
+ /** Unknown stage */
data object Unknown : StageViewModel()
+ /** This is the stage that moves the fingerprint icon around during enrollment. */
data object Guided : StageViewModel()
+ /** The center stage is the initial stage of enrollment. */
data object Center : StageViewModel()
+ /**
+ * Fingerprint stage of enrollment. Typically there is some sort of indication that a user should
+ * be using their finger tip to enroll.
+ */
data object Fingertip : StageViewModel()
+ /**
+ * Left edge stage of enrollment. Typically there is an indication that a user should be using the
+ * left edge of their fingerprint.
+ */
data object LeftEdge : StageViewModel()
+ /**
+ * Right edge stage of enrollment. Typically there is an indication that a user should be using
+ * the right edge of their fingerprint.
+ */
data object RightEdge : StageViewModel()
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt
new file mode 100644
index 00000000000..e349cebd2eb
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.viewmodel
+
+/** A class indicating a udfps enroll event occurred. */
+sealed class UdfpsEnrollEvent
+
+/** Describes how many [remainingSteps] and how many [totalSteps] are left in udfps enrollment. */
+data class UdfpsProgress(val remainingSteps: Int, val totalSteps: Int) : UdfpsEnrollEvent()
+
+/** Indicates a help event has been sent by enrollment */
+data class UdfpsHelp(val helpMsgId: Int, val helpString: String) : UdfpsEnrollEvent()
+
+/** Indicates a error event has been sent by enrollment */
+data class UdfpsError(val errMsgId: Int, val errString: String) : UdfpsEnrollEvent()
+
+/** Indicates an acquired event has occurred */
+data class Acquired(val acquiredGood: Boolean) : UdfpsEnrollEvent()
+
+/** Indicates a pointer down event has occurred */
+data object PointerDown : UdfpsEnrollEvent()
+
+/** Indicates a pointer up event has occurred */
+data object PointerUp : UdfpsEnrollEvent()
+
+/** Indicates the overlay has shown */
+data object OverlayShown : UdfpsEnrollEvent()
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
index 4fc3d1c5446..a5fdf1c85e0 100644
--- a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsViewModel.kt
@@ -16,16 +16,93 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
+import android.graphics.Rect
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
/** ViewModel used to drive UDFPS Enrollment through [UdfpsEnrollFragment] */
class UdfpsViewModel() : ViewModel() {
- /** Indicates what stage UDFPS enrollment is in. */
- val stageFlow = flowOf(StageViewModel.Center)
+ private val isSetupWizard = flowOf(false)
+
+ /** Indicates which Enrollment stage we are currently in. */
+ private val sensorLocationInternal = Pair(540, 1713)
+ private val sensorRadius = 100
+ private val sensorRect =
+ Rect(
+ this.sensorLocationInternal.first - sensorRadius,
+ this.sensorLocationInternal.second - sensorRadius,
+ this.sensorLocationInternal.first + sensorRadius,
+ this.sensorLocationInternal.second + sensorRadius,
+ )
+
+ private val stageThresholds = flowOf(listOf(.25, .5, .75, .875))
+
+ /** Indicates if accessibility is enabled */
+ val accessibilityEnabled = flowOf(false)
+
+ /** Indicates the locates of the fingerprint sensor. */
+ val sensorLocation: Flow = flowOf(sensorRect)
+
+ /** This is currently not hooked up to fingerprint manager, and is being fed mock events. */
+ val udfpsEvent: Flow =
+ flow {
+ enrollEvents.forEach { events ->
+ events.forEach { event -> emit(event) }
+ delay(1000)
+ }
+ }
+ .flowOn(Dispatchers.IO)
+
+ /** Determines the current [StageViewModel] enrollment is in */
+ val enrollStage: Flow =
+ combine(stageThresholds, udfpsEvent) { thresholds, event ->
+ if (event is UdfpsProgress) {
+ thresholdToStageMap(thresholds, event.totalSteps - event.remainingSteps, event.totalSteps)
+ } else {
+ null
+ }
+ }
+ .filterNotNull()
+
+ /** The header text for UDFPS enrollment */
+ val headerText: Flow =
+ combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
+ return@combine HeaderText(isSuw, isAccessibility, stage)
+ }
+
+ private val shouldClearDescriptionText = enrollStage.map { it is StageViewModel.Unknown }
+
+ /** The description text for UDFPS enrollment */
+ val descriptionText: Flow =
+ combine(isSetupWizard, accessibilityEnabled, enrollStage, shouldClearDescriptionText) {
+ isSuw,
+ isAccessibility,
+ stage,
+ shouldClearText ->
+ if (shouldClearText) {
+ return@combine null
+ } else {
+ return@combine DescriptionText(isSuw, isAccessibility, stage)
+ }
+ }
+
+ /** The lottie that should be shown for UDFPS Enrollment */
+ val lottie: Flow =
+ combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
+ return@combine EducationAnimationModel(isSuw, isAccessibility, stage)
+ }.distinctUntilChanged()
class UdfpsEnrollmentFactory() : ViewModelProvider.Factory {
@@ -38,5 +115,58 @@ class UdfpsViewModel() : ViewModel() {
companion object {
private val navStep = FingerprintNavigationStep.Enrollment::class
private const val TAG = "UDFPSViewModel"
+ private val ENROLLMENT_STAGES_ORDERED =
+ listOf(
+ StageViewModel.Center,
+ StageViewModel.Guided,
+ StageViewModel.Fingertip,
+ StageViewModel.LeftEdge,
+ StageViewModel.RightEdge,
+ )
+
+ /**
+ * [thresholds] is a list of 4 numbers from [0,1] that separate enrollment into 5 stages. The
+ * stage is determined by mapping [thresholds] * [maxSteps] and finding where the [currentStep]
+ * is.
+ *
+ * Each number in the array should be strictly increasing such as [0.2, 0.5, 0.6, 0.8]
+ */
+ private fun thresholdToStageMap(
+ thresholds: List,
+ currentStep: Int,
+ maxSteps: Int,
+ ): StageViewModel {
+ val stageIterator = ENROLLMENT_STAGES_ORDERED.iterator()
+ thresholds.forEach {
+ val thresholdLimit = it * maxSteps
+ val curr = stageIterator.next()
+ if (currentStep < thresholdLimit) {
+ return curr
+ }
+ }
+ return stageIterator.next()
+ }
+
+ /** This will be removed */
+ private val enrollEvents: List> =
+ listOf(
+ listOf(OverlayShown),
+ CreateProgress(10, 10),
+ CreateProgress(9, 10),
+ CreateProgress(8, 10),
+ CreateProgress(7, 10),
+ CreateProgress(6, 10),
+ CreateProgress(5, 10),
+ CreateProgress(4, 10),
+ CreateProgress(3, 10),
+ CreateProgress(2, 10),
+ CreateProgress(1, 10),
+ CreateProgress(0, 10),
+ )
+
+ /** This will be removed */
+ private fun CreateProgress(remaining: Int, total: Int): List {
+ return listOf(PointerDown, Acquired(true), UdfpsProgress(remaining, total), PointerUp)
+ }
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt
new file mode 100644
index 00000000000..5d4607c4f05
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.widget
+
+import android.content.Context
+import android.graphics.PointF
+import android.util.TypedValue
+import android.view.accessibility.AccessibilityManager
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
+
+/** Keeps track of which guided enrollment point we should be using */
+class UdfpsEnrollHelperV2(private val mContext: Context) {
+
+ private var isGuidedEnrollment: Boolean = false
+ private val accessibilityEnabled: Boolean
+ private val guidedEnrollmentPoints: MutableList
+ private var index = 0
+
+ init {
+ val am = mContext.getSystemService(AccessibilityManager::class.java)
+ accessibilityEnabled = am!!.isEnabled
+ guidedEnrollmentPoints = ArrayList()
+
+ // Number of pixels per mm
+ val px =
+ TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, mContext.resources.displayMetrics)
+ guidedEnrollmentPoints.add(PointF(2.00f * px, 0.00f * px))
+ guidedEnrollmentPoints.add(PointF(0.87f * px, -2.70f * px))
+ guidedEnrollmentPoints.add(PointF(-1.80f * px, -1.31f * px))
+ guidedEnrollmentPoints.add(PointF(-1.80f * px, 1.31f * px))
+ guidedEnrollmentPoints.add(PointF(0.88f * px, 2.70f * px))
+ guidedEnrollmentPoints.add(PointF(3.94f * px, -1.06f * px))
+ guidedEnrollmentPoints.add(PointF(2.90f * px, -4.14f * px))
+ guidedEnrollmentPoints.add(PointF(-0.52f * px, -5.95f * px))
+ guidedEnrollmentPoints.add(PointF(-3.33f * px, -3.33f * px))
+ guidedEnrollmentPoints.add(PointF(-3.99f * px, -0.35f * px))
+ guidedEnrollmentPoints.add(PointF(-3.62f * px, 2.54f * px))
+ guidedEnrollmentPoints.add(PointF(-1.49f * px, 5.57f * px))
+ guidedEnrollmentPoints.add(PointF(2.29f * px, 4.92f * px))
+ guidedEnrollmentPoints.add(PointF(3.82f * px, 1.78f * px))
+ }
+
+ /**
+ * This indicates whether we should be offsetting the enrollment icon based on
+ * [guidedEnrollmentPoints]
+ */
+ fun onUpdateStage(stage: StageViewModel) {
+ this.isGuidedEnrollment = stage is StageViewModel.Guided
+ }
+
+ /** Updates [index] to be used by [guidedEnrollmentPoints] */
+ fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
+ index = totalSteps - remaining
+ }
+
+ /**
+ * Returns the current guided enrollment point, or (0,0) if we are not in guided enrollment or are
+ * in accessibility.
+ */
+ val guidedEnrollmentLocation: PointF?
+ get() {
+ if (accessibilityEnabled || !isGuidedEnrollment) {
+ return null
+ }
+ var scale = SCALE
+ val originalPoint = guidedEnrollmentPoints[index % guidedEnrollmentPoints.size]
+ return PointF(originalPoint.x * scale, originalPoint.y * scale)
+ }
+
+ companion object {
+ private const val TAG = "UdfpsEnrollHelperV2"
+ private const val SCALE = 0.5f
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
new file mode 100644
index 00000000000..3b48140557e
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.widget
+
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.annotation.ColorInt
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.PathShape
+import android.util.AttributeSet
+import android.util.Log
+import android.util.PathParser
+import android.view.animation.AccelerateDecelerateInterpolator
+import androidx.core.animation.addListener
+import androidx.core.graphics.toRect
+import androidx.core.graphics.toRectF
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
+import kotlin.math.sin
+
+/**
+ * This class is responsible for drawing the udfps icon, and to update its movement based on the
+ * various stages of enrollment
+ */
+class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeSet?) : Drawable() {
+ private var targetAnimatorSet: AnimatorSet? = null
+ private val movingTargetFpIcon: Drawable
+ private val fingerprintDrawable: ShapeDrawable
+ private val sensorOutlinePaint: Paint
+ private val blueFill: Paint
+ private val helper = UdfpsEnrollHelperV2(context)
+ @ColorInt private var enrollIconColor = 0
+ @ColorInt private var movingTargetFill = 0
+ private var currentScale = 1.0f
+ private var alpha = 0
+
+ /**
+ * This is the physical location of the sensor. This rect will be updated by [drawSensorRectAt]
+ */
+ private val sensorRectBounds: Rect = Rect()
+
+ /**
+ * The following values are used to describe where the icon should be drawn. [currX] and [currY]
+ * are changed based on the current guided enrollment step which is given by the
+ * [UdfpsEnrollHelperV2]
+ */
+ private var currX = 0f
+ private var currY = 0f
+
+ private var sensorWidth = 0f
+ private var sensorHeight = 0f
+
+ init {
+ fingerprintDrawable = createUdfpsIcon(context)
+ context
+ .obtainStyledAttributes(
+ attrs,
+ R.styleable.BiometricsEnrollView,
+ R.attr.biometricsEnrollStyle,
+ R.style.BiometricsEnrollStyle,
+ )
+ .let {
+ enrollIconColor = it.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0)
+ movingTargetFill =
+ it.getColor(R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0)
+ it.recycle()
+ }
+
+ sensorOutlinePaint = Paint(0 /* flags */).apply {
+ isAntiAlias = true
+ setColor(movingTargetFill)
+ style = Paint.Style.FILL
+ }
+
+ blueFill = Paint(0 /* flags */).apply {
+ isAntiAlias = true
+ setColor(movingTargetFill)
+ style = Paint.Style.FILL
+ }
+
+ movingTargetFpIcon = context.resources.getDrawable(R.drawable.ic_enrollment_fingerprint, null).apply {
+ setTint(enrollIconColor)
+ mutate()
+ }
+
+ fingerprintDrawable.setTint(enrollIconColor)
+ setAlpha(255)
+ }
+
+ override fun getAlpha(): Int {
+ return alpha
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {}
+
+ override fun getOpacity(): Int {
+ return PixelFormat.UNKNOWN
+ }
+
+ override fun setAlpha(alpha: Int) {
+ this.alpha = alpha
+ }
+
+ /**
+ * The [sensorRect] coordinates for the sensor area. The [sensorRect] should be the coordinates
+ * with respect to its root frameview
+ */
+ fun drawSensorRectAt(sensorRect: Rect) {
+ Log.e(TAG, "UdfpsEnrollIcon#drawSensorRect($sensorRect)")
+ sensorRectBounds.set(sensorRect)
+ fingerprintDrawable.bounds = sensorRect
+ movingTargetFpIcon.bounds = sensorRect
+ currX = sensorRect.left.toFloat()
+ currY = sensorRect.top.toFloat()
+ sensorWidth = (sensorRect.right - sensorRect.left).toFloat()
+ sensorHeight = (sensorRect.bottom - sensorRect.top).toFloat()
+ invalidateSelf()
+ }
+
+ /** Update the progress of the icon */
+ fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
+ helper.onEnrollmentProgress(remaining, totalSteps)
+ val offset = helper.guidedEnrollmentLocation
+ val currentBounds = getCurrLocation().toRect()
+ if (offset != null) {
+ // This is the desired location of the sensor rect, the [EnrollHelper]
+ // offsets the initial sensor rect by a bit to get the user to move their finger a bit more.
+ val targetRect = Rect(sensorRectBounds).toRectF()
+ targetRect.offset(offset.x, offset.y)
+ var shouldAnimateMovement =
+ !currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f
+ if (shouldAnimateMovement) {
+ targetAnimatorSet?.let { it.cancel() }
+ animateMovement(currentBounds, targetRect, true)
+ }
+ } else {
+ // If we are not offsetting the sensor, move it back to its original place
+ animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
+ }
+
+ invalidateSelf()
+ }
+
+ /** Update the stage of the icon */
+ fun updateStage(it: StageViewModel) {
+ helper.onUpdateStage(it)
+ invalidateSelf()
+ }
+
+ override fun draw(canvas: Canvas) {
+ val currLocation = getCurrLocation()
+ canvas.scale(currentScale, currentScale, currLocation.centerX(), currLocation.centerY())
+
+ sensorRectBounds?.let { canvas.drawOval(currLocation, sensorOutlinePaint) }
+ fingerprintDrawable.bounds = currLocation.toRect()
+ fingerprintDrawable.draw(canvas)
+ fingerprintDrawable.setAlpha(alpha)
+ sensorOutlinePaint.setAlpha(alpha)
+ }
+
+ private fun getCurrLocation(): RectF =
+ RectF(currX, currY, currX + sensorWidth, currY + sensorHeight)
+
+ private fun animateMovement(currentBounds: Rect, offsetRect: RectF, scaleMovement: Boolean) {
+ if (currentBounds.equals(offsetRect)) {
+ return
+ }
+ val xAnimator = ValueAnimator.ofFloat(currentBounds.left.toFloat(), offsetRect.left)
+ xAnimator.addUpdateListener {
+ currX = it.animatedValue as Float
+ invalidateSelf()
+ }
+
+ val yAnimator = ValueAnimator.ofFloat(currentBounds.top.toFloat(), offsetRect.top)
+ yAnimator.addUpdateListener {
+ currY = it.animatedValue as Float
+ invalidateSelf()
+ }
+ val animators = mutableListOf(xAnimator, yAnimator)
+ val duration = TARGET_ANIM_DURATION_LONG
+ if (scaleMovement) {
+ val scaleAnimator = ValueAnimator.ofFloat(0f, Math.PI.toFloat())
+ scaleAnimator.setDuration(duration)
+ scaleAnimator.addUpdateListener { animation: ValueAnimator ->
+ // Grow then shrink
+ currentScale =
+ (1 + SCALE_MAX * sin((animation.getAnimatedValue() as Float).toDouble()).toFloat())
+ invalidateSelf()
+ }
+ scaleAnimator.addListener(onEnd = { currentScale = 1f })
+ animators.add(scaleAnimator)
+ }
+
+ targetAnimatorSet = AnimatorSet()
+
+ targetAnimatorSet?.let {
+ it.interpolator = AccelerateDecelerateInterpolator()
+ it.setDuration(duration)
+ it.playTogether(animators.toList())
+ it.start()
+ }
+ }
+
+ companion object {
+ private const val TAG = "UdfpsEnrollDrawableV2"
+ private const val DEFAULT_STROKE_WIDTH = 3f
+ private const val TARGET_ANIM_DURATION_LONG = 800L
+ private const val SCALE_MAX = 0.25f
+
+ private fun createUdfpsIcon(context: Context): ShapeDrawable {
+ val fpPath = context.resources.getString(R.string.config_udfpsIcon)
+ val drawable = ShapeDrawable(PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)).apply {
+ mutate()
+ paint.style = Paint.Style.STROKE
+ paint.strokeCap = Paint.Cap.ROUND
+ paint.strokeWidth = DEFAULT_STROKE_WIDTH
+ }
+ return drawable
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
new file mode 100644
index 00000000000..e5f89cb8efc
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.widget
+
+import android.animation.ValueAnimator
+import android.animation.ValueAnimator.AnimatorUpdateListener
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.os.Process
+import android.os.VibrationAttributes
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.util.AttributeSet
+import android.util.DisplayMetrics
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import androidx.annotation.ColorInt
+import androidx.core.graphics.toRectF
+import com.android.internal.annotations.VisibleForTesting
+import com.android.settings.R
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * UDFPS enrollment progress bar. This view is responsible for drawing the progress ring and its
+ * fill around the center of the UDFPS sensor.
+ */
+class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: AttributeSet?) :
+ Drawable() {
+ private val sensorRect: Rect = Rect()
+ private val strokeWidthPx: Float
+
+ @ColorInt private val progressColor: Int
+ @ColorInt private var helpColor: Int = 0
+ @ColorInt private val onFirstBucketFailedColor: Int
+
+ private val backgroundPaint: Paint
+
+ @VisibleForTesting val fillPaint: Paint
+ private val vibrator: Vibrator
+ private var isAccessibilityEnabled: Boolean = false
+ private var afterFirstTouch = false
+ private var remainingSteps = 0
+ private var totalSteps = 0
+ private var progress = 0f
+ private var progressAnimator: ValueAnimator? = null
+ private val progressUpdateListener: AnimatorUpdateListener
+ private var showingHelp = false
+ private var fillColorAnimator: ValueAnimator? = null
+ private val fillColorUpdateListener: AnimatorUpdateListener
+ private var backgroundColorAnimator: ValueAnimator? = null
+ private val backgroundColorUpdateListener: AnimatorUpdateListener
+ private var complete = false
+ private var movingTargetFill = 0
+ private var movingTargetFillError = 0
+ private var enrollProgressColor = 0
+ private var enrollProgressHelp = 0
+ private var enrollProgressHelpWithTalkback = 0
+ private val progressBarRadius: Int
+
+ init {
+ val ta =
+ mContext.obtainStyledAttributes(
+ attrs,
+ R.styleable.BiometricsEnrollView,
+ R.attr.biometricsEnrollStyle,
+ R.style.BiometricsEnrollStyle,
+ )
+ movingTargetFill = ta.getColor(R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0)
+ movingTargetFillError =
+ ta.getColor(R.styleable.BiometricsEnrollView_biometricsMovingTargetFillError, 0)
+ enrollProgressColor = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollProgress, 0)
+ enrollProgressHelp =
+ ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelp, 0)
+ enrollProgressHelpWithTalkback =
+ ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0)
+ ta.recycle()
+ val density = mContext.resources.displayMetrics.densityDpi.toFloat()
+ strokeWidthPx = STROKE_WIDTH_DP * (density / DisplayMetrics.DENSITY_DEFAULT)
+ progressColor = enrollProgressColor
+ onFirstBucketFailedColor = movingTargetFillError
+ updateHelpColor()
+ backgroundPaint = Paint().apply {
+ strokeWidth = strokeWidthPx
+ setColor(movingTargetFill)
+ isAntiAlias = true
+ style = Paint.Style.STROKE
+ strokeCap = Paint.Cap.ROUND
+ }
+
+ // Progress fill should *not* use the extracted system color.
+ fillPaint = Paint().apply {
+ strokeWidth = strokeWidthPx
+ setColor(progressColor)
+ isAntiAlias = true
+ style = Paint.Style.STROKE
+ strokeCap = Paint.Cap.ROUND
+ }
+ vibrator = mContext.getSystemService(Vibrator::class.java)!!
+
+ progressBarRadius = mContext.resources.getInteger(R.integer.config_udfpsEnrollProgressBar)
+
+ progressUpdateListener = AnimatorUpdateListener { animation: ValueAnimator ->
+ progress = animation.getAnimatedValue() as Float
+ invalidateSelf()
+ }
+ fillColorUpdateListener = AnimatorUpdateListener { animation: ValueAnimator ->
+ fillPaint.setColor(animation.getAnimatedValue() as Int)
+ invalidateSelf()
+ }
+ backgroundColorUpdateListener = AnimatorUpdateListener { animation: ValueAnimator ->
+ backgroundPaint.setColor(animation.getAnimatedValue() as Int)
+ invalidateSelf()
+ }
+ }
+
+ /** Indicates enrollment progress has occurred. */
+ fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
+ afterFirstTouch = true
+ updateState(remaining, totalSteps, false /* showingHelp */)
+ }
+
+ /** Indicates enrollment help has occurred. */
+ fun onEnrollmentHelp(remaining: Int, totalSteps: Int) {
+ updateState(remaining, totalSteps, true /* showingHelp */)
+ }
+
+ /** Indicates the last step was acquired. */
+ fun onLastStepAcquired() {
+ updateState(0, totalSteps, false /* showingHelp */)
+ }
+
+ override fun draw(canvas: Canvas) {
+
+ canvas.save()
+ // This takes the sensors bounding box and expands it by [progressBarRadius] in all directions
+ val sensorProgressRect = Rect(sensorRect)
+ sensorProgressRect.inset(
+ -progressBarRadius,
+ -progressBarRadius,
+ -progressBarRadius,
+ -progressBarRadius,
+ )
+
+ // Rotate -90 degrees to make the progress start from the top right and not the bottom
+ // right
+ canvas.rotate(
+ -90f,
+ sensorProgressRect.centerX().toFloat(),
+ sensorProgressRect.centerY().toFloat(),
+ )
+ if (progress < 1f) {
+ // Draw the background color of the progress circle.
+ canvas.drawArc(
+ sensorProgressRect.toRectF(),
+ 0f /* startAngle */,
+ 360f /* sweepAngle */,
+ false /* useCenter */,
+ backgroundPaint,
+ )
+ }
+ if (progress > 0f) {
+ // Draw the filled portion of the progress circle.
+ canvas.drawArc(
+ sensorProgressRect.toRectF(),
+ 0f /* startAngle */,
+ 360f * progress /* sweepAngle */,
+ false /* useCenter */,
+ fillPaint,
+ )
+ }
+ canvas.restore()
+ }
+
+ /** Do nothing here, we will control the alpha internally. */
+ override fun setAlpha(alpha: Int) {}
+
+ /** Do not apply color filters here for enrollment. */
+ override fun setColorFilter(colorFilter: ColorFilter?) {}
+
+ /** Legacy code, returning PixelFormat.UNKNOWN */
+ override fun getOpacity(): Int {
+ return PixelFormat.UNKNOWN
+ }
+ /**
+ * Draws the progress with locations [sensorLocationX] [sensorLocationY], note these must be with
+ * respect to the parent framelayout.
+ */
+ fun drawProgressAt(sensorRect: Rect) {
+ this.sensorRect.set(sensorRect)
+ }
+
+ /** Indicates if accessibility is enabled or not. */
+ fun setAccessibilityEnabled(enabled: Boolean) {
+ isAccessibilityEnabled = enabled
+ updateHelpColor()
+ }
+
+ private fun updateHelpColor() {
+ helpColor =
+ if (!isAccessibilityEnabled) {
+ enrollProgressHelp
+ } else {
+ enrollProgressHelpWithTalkback
+ }
+ }
+
+ private fun updateState(remainingSteps: Int, totalSteps: Int, showingHelp: Boolean) {
+ updateProgress(remainingSteps, totalSteps, showingHelp)
+ updateFillColor(showingHelp)
+ }
+
+ private fun updateProgress(remainingSteps: Int, totalSteps: Int, showingHelp: Boolean) {
+ if (this.remainingSteps == remainingSteps && this.totalSteps == totalSteps) {
+ return
+ }
+ if (this.showingHelp) {
+ if (vibrator != null && isAccessibilityEnabled) {
+ vibrator.vibrate(
+ Process.myUid(),
+ mContext.opPackageName,
+ VIBRATE_EFFECT_ERROR,
+ javaClass.getSimpleName() + "::onEnrollmentHelp",
+ FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES,
+ )
+ }
+ } else {
+ // If the first touch is an error, remainingSteps will be -1 and the callback
+ // doesn't come from onEnrollmentHelp. If we are in the accessibility flow,
+ // we still would like to vibrate.
+ if (vibrator != null) {
+ if (remainingSteps == -1 && isAccessibilityEnabled) {
+ vibrator.vibrate(
+ Process.myUid(),
+ mContext.opPackageName,
+ VIBRATE_EFFECT_ERROR,
+ javaClass.getSimpleName() + "::onFirstTouchError",
+ FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES,
+ )
+ } else if (remainingSteps != -1 && !isAccessibilityEnabled) {
+ vibrator.vibrate(
+ Process.myUid(),
+ mContext.opPackageName,
+ SUCCESS_VIBRATION_EFFECT,
+ javaClass.getSimpleName() + "::OnEnrollmentProgress",
+ HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES,
+ )
+ }
+ }
+ }
+ this.showingHelp = showingHelp
+ this.remainingSteps = remainingSteps
+ this.totalSteps = totalSteps
+ val progressSteps = max(0.0, (totalSteps - remainingSteps).toDouble()).toInt()
+
+ // If needed, add 1 to progress and total steps to account for initial touch.
+ val adjustedSteps = if (afterFirstTouch) progressSteps + 1 else progressSteps
+ val adjustedTotal = if (afterFirstTouch) this.totalSteps + 1 else this.totalSteps
+ val targetProgress =
+ min(1.0, (adjustedSteps.toFloat() / adjustedTotal.toFloat()).toDouble()).toFloat()
+ if (progressAnimator != null && progressAnimator!!.isRunning) {
+ progressAnimator!!.cancel()
+ }
+ progressAnimator =
+ ValueAnimator.ofFloat(progress, targetProgress).also {
+ it.setDuration(PROGRESS_ANIMATION_DURATION_MS)
+ it.addUpdateListener(progressUpdateListener)
+ it.start()
+ }
+ if (remainingSteps == 0) {
+ startCompletionAnimation()
+ } else if (remainingSteps > 0) {
+ rollBackCompletionAnimation()
+ }
+ }
+
+ private fun animateBackgroundColor() {
+ if (backgroundColorAnimator != null && backgroundColorAnimator!!.isRunning) {
+ backgroundColorAnimator!!.end()
+ }
+ backgroundColorAnimator =
+ ValueAnimator.ofArgb(backgroundPaint.color, onFirstBucketFailedColor).also {
+ it.setDuration(FILL_COLOR_ANIMATION_DURATION_MS)
+ it.repeatCount = 1
+ it.repeatMode = ValueAnimator.REVERSE
+ it.interpolator = DEACCEL
+ it.addUpdateListener(backgroundColorUpdateListener)
+ it.start()
+ }
+ }
+
+ private fun updateFillColor(showingHelp: Boolean) {
+ if (!afterFirstTouch && showingHelp) {
+ // If we are on the first touch, animate the background color
+ // instead of the progress color.
+ animateBackgroundColor()
+ return
+ }
+ if (fillColorAnimator != null && fillColorAnimator!!.isRunning) {
+ fillColorAnimator!!.end()
+ }
+ @ColorInt val targetColor = if (showingHelp) helpColor else progressColor
+ fillColorAnimator =
+ ValueAnimator.ofArgb(fillPaint.color, targetColor).also {
+ it.setDuration(FILL_COLOR_ANIMATION_DURATION_MS)
+ it.repeatCount = 1
+ it.repeatMode = ValueAnimator.REVERSE
+ it.interpolator = DEACCEL
+ it.addUpdateListener(fillColorUpdateListener)
+ it.start()
+ }
+ }
+
+ private fun startCompletionAnimation() {
+ if (complete) {
+ return
+ }
+ complete = true
+ }
+
+ private fun rollBackCompletionAnimation() {
+ if (!complete) {
+ return
+ }
+ complete = false
+ }
+
+ private fun loadResources(context: Context, attrs: AttributeSet?) {}
+
+ companion object {
+ private const val TAG = "UdfpsProgressBar"
+ private const val FILL_COLOR_ANIMATION_DURATION_MS = 350L
+ private const val PROGRESS_ANIMATION_DURATION_MS = 400L
+ private const val STROKE_WIDTH_DP = 12f
+ private val DEACCEL: Interpolator = DecelerateInterpolator()
+ private val VIBRATE_EFFECT_ERROR = VibrationEffect.createWaveform(longArrayOf(0, 5, 55, 60), -1)
+ private val FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY)
+ private val HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
+ VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)
+ private val SUCCESS_VIBRATION_EFFECT = VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
new file mode 100644
index 00000000000..38bb02494a7
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 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.modules.enrolling.udfps.ui.widget
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.util.Log
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.Acquired
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.OverlayShown
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.PointerDown
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.PointerUp
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.StageViewModel
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsEnrollEvent
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsError
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsHelp
+import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel.UdfpsProgress
+
+/**
+ * View corresponding with fingerprint_v2_udfps_enroll_view.xml. This view is responsible for
+ * drawing the [UdfpsEnrollIconV2] and the [UdfpsEnrollProgressBarDrawableV2].
+ */
+class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
+ private var isAccessibilityEnabled: Boolean = false
+ private lateinit var sensorRect: Rect
+ private val fingerprintIcon: UdfpsEnrollIconV2 = UdfpsEnrollIconV2(mContext, attrs)
+ private val fingerprintProgressDrawable: UdfpsEnrollProgressBarDrawableV2 =
+ UdfpsEnrollProgressBarDrawableV2(mContext, attrs)
+ private var mTotalSteps = -1
+ private var mRemainingSteps = -1
+
+ /**
+ * This function computes the center (x,y) location with respect to the parent [FrameLayout] for
+ * the [UdfpsEnrollProgressBarDrawableV2]. It also computes the [Rect] with respect to the parent
+ * [FrameLayout] for the [UdfpsEnrollIconV2].
+ */
+ fun setSensorRect(rect: Rect) {
+ this.sensorRect = rect
+ post {
+ findViewById(R.id.udfps_enroll_animation_fp_progress_view)?.also {
+ it.setImageDrawable(fingerprintProgressDrawable)
+ }
+ findViewById(R.id.udfps_enroll_animation_fp_view)?.also {
+ it.setImageDrawable(fingerprintIcon)
+ }
+ val parentView = parent as ViewGroup
+ val coords = parentView.getLocationOnScreen()
+ val parentLeft = coords[0]
+ val parentTop = coords[1]
+ val sensorRectOffset = Rect(sensorRect)
+ sensorRectOffset.offset(-parentLeft, -parentTop)
+
+ fingerprintIcon.drawSensorRectAt(sensorRectOffset)
+ fingerprintProgressDrawable.drawProgressAt(sensorRectOffset)
+ }
+ }
+
+ /** Updates the current enrollment stage. */
+ fun updateStage(it: StageViewModel) {
+ fingerprintIcon.updateStage(it)
+ }
+
+ /** Receive enroll progress event */
+ fun onUdfpsEvent(event: UdfpsEnrollEvent) {
+ when (event) {
+ is UdfpsProgress -> onEnrollmentProgress(event.remainingSteps, event.totalSteps)
+ is Acquired -> onAcquired(event.acquiredGood)
+ is UdfpsHelp -> onEnrollmentHelp()
+ is PointerDown -> onPointerDown()
+ is PointerUp -> onPointerUp()
+ OverlayShown -> overlayShown()
+ is UdfpsError -> udfpsError(event.errMsgId, event.errString)
+ }
+ }
+
+ /** Indicates if accessibility is enabled. */
+ fun setAccessibilityEnabled(enabled: Boolean) {
+ this.isAccessibilityEnabled = enabled
+ fingerprintProgressDrawable.setAccessibilityEnabled(enabled)
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ invalidate()
+ super.onLayout(changed, left, top, right, bottom)
+ invalidate()
+ }
+
+ private fun udfpsError(errMsgId: Int, errString: String) {
+ Log.e(TAG, "Implement udfpsError")
+ }
+
+ private fun overlayShown() {
+ Log.e(TAG, "Implement overlayShown")
+ }
+
+ /** Receive enroll progress event */
+ private fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
+ post {
+ fingerprintIcon.onEnrollmentProgress(remaining, totalSteps)
+ fingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps)
+ }
+ }
+
+ /** Receive enroll help event */
+ private fun onEnrollmentHelp() {
+ post { fingerprintProgressDrawable.onEnrollmentHelp(mRemainingSteps, mTotalSteps) }
+ }
+
+ /** Receive onAcquired event */
+ private fun onAcquired(isAcquiredGood: Boolean) {
+ val animateIfLastStepGood = isAcquiredGood && mRemainingSteps <= 2 && mRemainingSteps >= 0
+ post { if (animateIfLastStepGood) fingerprintProgressDrawable.onLastStepAcquired() }
+ }
+
+ /** Receive onPointerDown event */
+ private fun onPointerDown() {}
+
+ /** Receive onPointerUp event */
+ private fun onPointerUp() {}
+
+ companion object {
+ private const val TAG = "UdfpsEnrollView"
+ }
+}