From a811dd67bd30d2ff939010318dc34fc4aa9399f0 Mon Sep 17 00:00:00 2001 From: Joshua McCloskey Date: Wed, 28 Feb 2024 21:57:56 +0000 Subject: [PATCH] UDFPS Enrollment refactor (2/N) This commit is focused on UI. The guided enrollment, lottie, text and progress bar should all be working according to the previous experience. Test: atest Bug: 297082837 Change-Id: I9b414053f5eaf7b2bc164dacdddc96ed44fec6cb --- .../fingerprint_v2_udfps_enroll_enrolling.xml | 101 +++-- .../fingerprint_v2_udfps_enroll_view.xml | 38 ++ .../repository/FingerprintSensorRepository.kt | 45 ++- .../FingerprintManagerInteractorImpl.kt | 2 + .../FingerprintManagerInteractor.kt | 1 + .../rfps/ui/fragment/RFPSEnrollFragment.kt | 3 +- .../rfps/ui/widget/RFPSProgressBar.kt | 2 - .../udfps/ui/fragment/UdfpsEnrollFragment.kt | 186 +++++---- .../udfps/ui/viewmodel/DescriptionText.kt | 24 ++ .../ui/viewmodel/EducationAnimationModel.kt | 24 ++ .../udfps/ui/viewmodel/HeaderText.kt | 24 ++ .../udfps/ui/viewmodel/StageViewModel.kt | 15 + .../udfps/ui/viewmodel/UdfpsEnrollEvent.kt | 41 ++ .../udfps/ui/viewmodel/UdfpsViewModel.kt | 134 ++++++- .../udfps/ui/widget/UdfpsEnrollHelperV2.kt | 88 +++++ .../udfps/ui/widget/UdfpsEnrollIconV2.kt | 243 ++++++++++++ .../UdfpsEnrollProgressBarDrawableV2.kt | 362 ++++++++++++++++++ .../udfps/ui/widget/UdfpsEnrollViewV2.kt | 142 +++++++ 18 files changed, 1336 insertions(+), 139 deletions(-) create mode 100644 res/layout/fingerprint_v2_udfps_enroll_view.xml create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/DescriptionText.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/EducationAnimationModel.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/HeaderText.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/viewmodel/UdfpsEnrollEvent.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollHelperV2.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollIconV2.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollProgressBarDrawableV2.kt create mode 100644 src/com/android/settings/biometrics/fingerprint2/ui/enrollment/modules/enrolling/udfps/ui/widget/UdfpsEnrollViewV2.kt 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" + } +}