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
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2023 The Android Open Source Project
|
||||
~ 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.
|
||||
@@ -15,56 +15,77 @@
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<com.google.android.setupdesign.GlifLayout
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
||||
android:id="@+id/setup_wizard_layout"
|
||||
style="?attr/fingerprint_layout_theme"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
style="@style/SudContentFrame"
|
||||
<!-- This is used to grab style attributes and apply them
|
||||
to this layout -->
|
||||
<com.google.android.setupdesign.GlifLayout
|
||||
android:id="@+id/dummy_glif_layout"
|
||||
style="?attr/fingerprint_layout_theme"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sud_layout_icon"
|
||||
style="@style/SudGlifIcon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleType="fitStart"
|
||||
android:src="@drawable/ic_lock" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
style="@style/SudGlifHeaderTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:ellipsize="end"
|
||||
android:lines="2"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
style="@style/SudDescription.Glif"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="3"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
/>
|
||||
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/illustration_lottie"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:scaleType="centerInside"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_speed=".85" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center|bottom"
|
||||
android:orientation="vertical">
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:clipToPadding="false"
|
||||
>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/layout_container"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="center_horizontal|bottom"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
tools:ignore="Suspicious0dp">
|
||||
|
||||
<!-- Animation res MUST be set in code -->
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/illustration_lottie"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:scaleType="centerInside"
|
||||
app:lottie_autoPlay="true"
|
||||
app:lottie_loop="true"
|
||||
app:lottie_speed=".85" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
<include layout="@layout/fingerprint_v2_udfps_enroll_view" />
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</com.google.android.setupdesign.GlifLayout>
|
||||
</LinearLayout>
|
||||
|
38
res/layout/fingerprint_v2_udfps_enroll_view.xml
Normal file
38
res/layout/fingerprint_v2_udfps_enroll_view.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
|
||||
<com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/udfps_animation_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/udfps_enroll_animation_fp_progress_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/udfps_enroll_animation_fp_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget.UdfpsEnrollViewV2>
|
@@ -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<FingerprintSensor> =
|
||||
private val fingerprintPropsInternal: Flow<FingerprintSensorPropertiesInternal> =
|
||||
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<ComponentInfoInternal>(),
|
||||
FingerprintSensorProperties.TYPE_UNKNOWN,
|
||||
false /* halControlsIllumination */,
|
||||
true /* resetLockoutRequiresHardwareAuthToken */,
|
||||
listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT),
|
||||
)
|
||||
.toFingerprintSensor()
|
||||
override val fingerprintSensor: Flow<FingerprintSensor> =
|
||||
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<ComponentInfoInternal>(),
|
||||
FingerprintSensorProperties.TYPE_UNKNOWN,
|
||||
false /* halControlsIllumination */,
|
||||
true /* resetLockoutRequiresHardwareAuthToken */,
|
||||
listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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?,
|
||||
|
@@ -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) {
|
||||
|
@@ -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<TextView>(R.id.title)!!
|
||||
val descriptionTextView = view.findViewById<TextView>(R.id.description)!!
|
||||
|
||||
val glifLayout = view.findViewById<GlifLayout>(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
|
||||
|
@@ -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,
|
||||
)
|
@@ -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,
|
||||
)
|
@@ -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,
|
||||
)
|
@@ -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()
|
||||
}
|
||||
|
@@ -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()
|
@@ -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<Rect> = flowOf(sensorRect)
|
||||
|
||||
/** This is currently not hooked up to fingerprint manager, and is being fed mock events. */
|
||||
val udfpsEvent: Flow<UdfpsEnrollEvent> =
|
||||
flow {
|
||||
enrollEvents.forEach { events ->
|
||||
events.forEach { event -> emit(event) }
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
/** Determines the current [StageViewModel] enrollment is in */
|
||||
val enrollStage: Flow<StageViewModel> =
|
||||
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<HeaderText> =
|
||||
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<DescriptionText?> =
|
||||
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<EducationAnimationModel> =
|
||||
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<Double>,
|
||||
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<List<UdfpsEnrollEvent>> =
|
||||
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<UdfpsEnrollEvent> {
|
||||
return listOf(PointerDown, Acquired(true), UdfpsProgress(remaining, total), PointerUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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<PointF>
|
||||
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
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
@@ -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<ImageView?>(R.id.udfps_enroll_animation_fp_progress_view)?.also {
|
||||
it.setImageDrawable(fingerprintProgressDrawable)
|
||||
}
|
||||
findViewById<ImageView>(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"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user