Merge "UDFPS Enrollment Refactor (5/N)" into main

This commit is contained in:
Joshua Mccloskey
2024-05-21 16:59:33 +00:00
committed by Android (Google) Code Review
18 changed files with 391 additions and 212 deletions

View File

@@ -14,38 +14,38 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.lib.model
package com.android.settings.biometrics.fingerprint2.data.model
/**
* A view model that describes the various stages of UDFPS Enrollment. This stages typically update
* the enrollment UI in a major way, such as changing the lottie animation or changing the location
* of the where the user should press their fingerprint
*/
sealed class StageViewModel {
sealed class EnrollStageModel {
/** Unknown stage */
data object Unknown : StageViewModel()
data object Unknown : EnrollStageModel()
/** This is the stage that moves the fingerprint icon around during enrollment. */
data object Guided : StageViewModel()
data object Guided : EnrollStageModel()
/** The center stage is the initial stage of enrollment. */
data object Center : StageViewModel()
data object Center : EnrollStageModel()
/**
* 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()
data object Fingertip : EnrollStageModel()
/**
* 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()
data object LeftEdge : EnrollStageModel()
/**
* 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()
data object RightEdge : EnrollStageModel()
}

View File

@@ -16,11 +16,11 @@
package com.android.settings.biometrics.fingerprint2.domain.interactor
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
typealias EnrollStageThresholds = Map<Float, StageViewModel>
typealias EnrollStageThresholds = Map<Float, EnrollStageModel>
/** Interactor that provides enroll stages for enrollment. */
interface EnrollStageInteractor {
@@ -33,11 +33,11 @@ class EnrollStageInteractorImpl() : EnrollStageInteractor {
override val enrollStageThresholds: Flow<EnrollStageThresholds> =
flowOf(
mapOf(
0.0f to StageViewModel.Center,
0.25f to StageViewModel.Guided,
0.5f to StageViewModel.Fingertip,
0.75f to StageViewModel.LeftEdge,
0.875f to StageViewModel.RightEdge,
0.0f to EnrollStageModel.Center,
0.25f to EnrollStageModel.Guided,
0.5f to EnrollStageModel.Fingertip,
0.75f to EnrollStageModel.LeftEdge,
0.875f to EnrollStageModel.RightEdge,
)
)
}

View File

@@ -42,6 +42,7 @@ interface OrientationInteractor {
* A flow that contains the rotation info matched against the def [config_reverseDefaultRotation]
*/
val rotationFromDefault: Flow<Int>
/**
* A Helper function that computes rotation if device is in
* [R.bool.config_reverseDefaultConfigRotation]

View File

@@ -0,0 +1,96 @@
/*
* 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.domain.interactor
import android.graphics.PointF
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
/**
* This interactor provides information about the current offset of the sensor for guided enrollment
* on UDFPS devices.
*/
interface UdfpsEnrollInteractor {
/** Indicates at which step a UDFPS enrollment is in. */
fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int)
/** Indicates if guided enrollment should be enabled or not. */
fun updateGuidedEnrollment(enabled: Boolean)
/**
* A flow indicating how much the sensor image drawable should be offset for guided enrollment. A
* null point indicates that the icon should be in its default position.
*/
val guidedEnrollmentOffset: Flow<PointF>
}
/** Keeps track of which guided enrollment point we should be using */
class UdfpsEnrollInteractorImpl(
pixelsPerMillimeter: Float,
accessibilityInteractor: AccessibilityInteractor,
) : UdfpsEnrollInteractor {
private var isGuidedEnrollment = MutableStateFlow(false)
// Number of pixels per mm
val px = pixelsPerMillimeter
private val guidedEnrollmentPoints: MutableList<PointF> =
mutableListOf(
PointF(2.00f * px, 0.00f * px),
PointF(0.87f * px, -2.70f * px),
PointF(-1.80f * px, -1.31f * px),
PointF(-1.80f * px, 1.31f * px),
PointF(0.88f * px, 2.70f * px),
PointF(3.94f * px, -1.06f * px),
PointF(2.90f * px, -4.14f * px),
PointF(-0.52f * px, -5.95f * px),
PointF(-3.33f * px, -3.33f * px),
PointF(-3.99f * px, -0.35f * px),
PointF(-3.62f * px, 2.54f * px),
PointF(-1.49f * px, 5.57f * px),
PointF(2.29f * px, 4.92f * px),
PointF(3.82f * px, 1.78f * px),
)
override fun onEnrollmentStep(stepsRemaining: Int, totalStep: Int) {
val index = (totalStep - stepsRemaining) % guidedEnrollmentPoints.size
_guidedEnrollment.update { guidedEnrollmentPoints[index] }
}
override fun updateGuidedEnrollment(enabled: Boolean) {
isGuidedEnrollment.update { enabled }
}
private val _guidedEnrollment = MutableStateFlow(PointF(0f, 0f))
override val guidedEnrollmentOffset: Flow<PointF> =
combine(
_guidedEnrollment,
accessibilityInteractor.isAccessibilityEnabled,
isGuidedEnrollment,
) { point, accessibilityEnabled, guidedEnrollmentEnabled ->
if (accessibilityEnabled || !guidedEnrollmentEnabled) {
return@combine PointF(0f, 0f)
} else {
return@combine PointF(point.x * SCALE, point.y * SCALE)
}
}
companion object {
private const val SCALE = 0.5f
}
}

View File

@@ -24,6 +24,7 @@ import android.hardware.fingerprint.FingerprintManager
import android.os.Bundle
import android.os.Vibrator
import android.util.Log
import android.util.TypedValue
import android.view.accessibility.AccessibilityManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
@@ -54,6 +55,8 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateI
import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractorImpl
import com.android.settings.biometrics.fingerprint2.lib.model.Default
@@ -89,6 +92,7 @@ import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Fing
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintScrollViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.GatekeeperInfo
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.Transition
import com.android.settings.flags.Flags
import com.android.settings.password.ChooseLockGeneric
import com.android.settings.password.ChooseLockSettingsHelper
@@ -116,6 +120,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
private lateinit var foldStateInteractor: FoldStateInteractor
private lateinit var orientationInteractor: OrientationInteractor
private lateinit var displayDensityInteractor: DisplayDensityInteractor
private lateinit var udfpsEnrollInteractor: UdfpsEnrollInteractor
private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
private lateinit var backgroundViewModel: BackgroundViewModel
private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel
@@ -256,6 +261,15 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
fingerprintManager,
Settings,
)
val accessibilityInteractor =
AccessibilityInteractorImpl(
getSystemService(AccessibilityManager::class.java)!!,
lifecycleScope,
)
val pixelsPerMillimeter =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, context.resources.displayMetrics)
udfpsEnrollInteractor = UdfpsEnrollInteractorImpl(pixelsPerMillimeter, accessibilityInteractor)
val fingerprintManagerInteractor =
FingerprintManagerInteractorImpl(
@@ -273,12 +287,6 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
val hasConfirmedDeviceCredential = gatekeeperInfo is GatekeeperInfo.GatekeeperPasswordInfo
val accessibilityInteractor =
AccessibilityInteractorImpl(
getSystemService(AccessibilityManager::class.java)!!,
lifecycleScope,
)
navigationViewModel =
ViewModelProvider(
this,
@@ -384,6 +392,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
orientationInteractor,
backgroundViewModel,
fingerprintSensorRepo,
udfpsEnrollInteractor,
),
)[UdfpsViewModel::class.java]
@@ -435,17 +444,17 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
else -> FingerprintEnrollEnrollingV2Fragment()
}
}
Introduction -> FingerprintEnrollIntroV2Fragment()
is Introduction -> FingerprintEnrollIntroV2Fragment()
else -> null
}
if (theClass != null) {
supportFragmentManager.fragments.onEach { fragment ->
supportFragmentManager.beginTransaction().remove(fragment).commit()
}
supportFragmentManager
.beginTransaction()
.setCustomAnimations(
step.enterTransition.toAnimation(),
step.exitTransition.toAnimation(),
)
.setReorderingAllowed(true)
.add(R.id.fragment_container_view, theClass::class.java, null)
.commit()
@@ -512,3 +521,12 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
}
}
}
private fun Transition.toAnimation(): Int {
return when (this) {
Transition.EnterFromLeft -> com.google.android.setupdesign.R.anim.sud_slide_back_in
Transition.EnterFromRight -> com.google.android.setupdesign.R.anim.sud_slide_next_in
Transition.ExitToLeft -> com.google.android.setupdesign.R.anim.sud_slide_next_out
Transition.ExitToRight -> com.google.android.setupdesign.R.anim.sud_slide_back_out
}
}

View File

@@ -32,12 +32,12 @@ 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.data.model.EnrollStageModel
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog
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.model.DescriptionText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.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.HeaderText
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
@@ -83,6 +83,8 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
window.statusBarColor = color
view.setBackgroundColor(color)
udfpsEnrollView.setFinishAnimationCompleted { viewModel.finishedSuccessfully() }
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch {
@@ -159,7 +161,14 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.enrollStage.collect { udfpsEnrollView.updateStage(it) }
viewModel.guidedEnrollment.collect {
glifLayout.post { udfpsEnrollView.updateGuidedEnrollment(it) }
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.guidedEnrollmentSaved.collect {
glifLayout.post { udfpsEnrollView.onGuidedPointSaved(it) }
}
}
}
}
@@ -175,35 +184,35 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
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
return when (this.enrollStageModel) {
EnrollStageModel.Center,
EnrollStageModel.Guided,
EnrollStageModel.Fingertip,
EnrollStageModel.Unknown -> R.string.security_settings_udfps_enroll_fingertip_title
EnrollStageModel.LeftEdge -> R.string.security_settings_udfps_enroll_left_edge_title
EnrollStageModel.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
return when (this.enrollStageModel) {
EnrollStageModel.Center,
EnrollStageModel.Guided,
EnrollStageModel.Fingertip,
EnrollStageModel.LeftEdge,
EnrollStageModel.RightEdge -> null
EnrollStageModel.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
return when (this.enrollStageModel) {
EnrollStageModel.Center,
EnrollStageModel.Guided -> R.raw.udfps_center_hint_lottie
EnrollStageModel.Fingertip -> R.raw.udfps_tip_hint_lottie
EnrollStageModel.LeftEdge -> R.raw.udfps_left_edge_hint_lottie
EnrollStageModel.RightEdge -> R.raw.udfps_right_edge_hint_lottie
EnrollStageModel.Unknown -> null
}
}

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
/** Represents the description text for UDFPS enrollment */
data class DescriptionText(
val isSuw: Boolean,
val isAccessibility: Boolean,
val stageViewModel: StageViewModel,
val enrollStageModel: EnrollStageModel,
)

View File

@@ -14,13 +14,13 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
/** Represents the header text for UDFPS enrollment */
data class HeaderText(
val isSuw: Boolean,
val isAccessibility: Boolean,
val stageViewModel: StageViewModel,
val enrollStageModel: EnrollStageModel,
)

View File

@@ -16,11 +16,11 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
/** Represents the lottie for UDFPS enrollment */
data class EducationAnimationModel(
val isSuw: Boolean,
val isAccessibility: Boolean,
val stageViewModel: StageViewModel,
val enrollStageModel: EnrollStageModel,
)

View File

@@ -17,20 +17,24 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import android.graphics.Point
import android.graphics.PointF
import android.view.Surface
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
import com.android.settings.biometrics.fingerprint2.data.model.EnrollStageModel
import com.android.settings.biometrics.fingerprint2.data.repository.SimulatedTouchEventsRepository
import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintVibrationEffects
import com.android.settings.biometrics.fingerprint2.domain.interactor.OrientationInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.UdfpsEnrollInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.VibrationInteractor
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.DescriptionText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.model.HeaderText
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
@@ -61,9 +65,11 @@ class UdfpsViewModel(
orientationInteractor: OrientationInteractor,
backgroundViewModel: BackgroundViewModel,
sensorRepository: FingerprintSensorRepository,
udfpsEnrollInteractor: UdfpsEnrollInteractor,
) : ViewModel() {
private val isSetupWizard = flowOf(false)
private var shouldResetErollment = false
private var _enrollState: Flow<FingerEnrollState?> =
fingerprintEnrollEnrollingViewModel.enrollFlow
@@ -112,6 +118,17 @@ class UdfpsViewModel(
}
}
/**
* This indicates at which point the UI should offset the fingerprint sensor icon for guided
* enrollment.
*/
val guidedEnrollment: Flow<PointF> =
udfpsEnrollInteractor.guidedEnrollmentOffset.distinctUntilChanged()
/** The saved version of [guidedEnrollment] */
val guidedEnrollmentSaved: Flow<PointF> =
guidedEnrollment.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
/**
* This is the saved progress, this is for when views are recreated and need saved state for the
* first time.
@@ -132,13 +149,13 @@ class UdfpsViewModel(
}
}
/** Determines the current [StageViewModel] enrollment is in */
val enrollStage: Flow<StageViewModel> =
/** Determines the current [EnrollStageModel] enrollment is in */
private val enrollStage: Flow<EnrollStageModel> =
combine(enrollStageInteractor.enrollStageThresholds, enrollState) { thresholds, event ->
if (event is FingerEnrollState.EnrollProgress) {
val progress =
(event.totalStepsRequired - event.remainingSteps).toFloat() / event.totalStepsRequired
var stageToReturn: StageViewModel = StageViewModel.Center
var stageToReturn: EnrollStageModel = EnrollStageModel.Center
thresholds.forEach { (threshold, stage) ->
if (progress < threshold) {
return@forEach
@@ -153,6 +170,40 @@ class UdfpsViewModel(
.filterNotNull()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
init {
viewModelScope.launch {
enrollState
.combine(accessibilityEnabled) { event, isEnabled -> Pair(event, isEnabled) }
.collect {
if (
when (it.first) {
is FingerEnrollState.EnrollError -> true
is FingerEnrollState.EnrollHelp -> it.second
is FingerEnrollState.EnrollProgress -> true
else -> false
}
) {
vibrate(it.first)
}
}
}
viewModelScope.launch {
enrollStage.collect {
udfpsEnrollInteractor.updateGuidedEnrollment(it is EnrollStageModel.Guided)
}
}
viewModelScope.launch {
enrollState.filterIsInstance<FingerEnrollState.EnrollProgress>().collect {
udfpsEnrollInteractor.onEnrollmentStep(it.remainingSteps, it.totalStepsRequired)
}
}
viewModelScope.launch {
backgroundViewModel.background.filter { true }.collect { didGoToBackground() }
}
}
/** Indicates if we should show the lottie. */
val shouldShowLottie: Flow<Boolean> =
combine(
@@ -183,7 +234,7 @@ class UdfpsViewModel(
}
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
private val shouldClearDescriptionText = enrollStage.map { it is StageViewModel.Unknown }
private val shouldClearDescriptionText = enrollStage.map { it is EnrollStageModel.Unknown }
/** The description text for UDFPS enrollment */
val descriptionText: Flow<DescriptionText?> =
@@ -202,6 +253,10 @@ class UdfpsViewModel(
/** Indicates if the consumer is ready for enrollment */
fun readyForEnrollment() {
if (shouldResetErollment) {
shouldResetErollment = false
_enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
}
fingerprintEnrollEnrollingViewModel.canEnroll()
}
@@ -237,8 +292,12 @@ class UdfpsViewModel(
}
private fun doReset() {
/** Indicates if the icon should be animating or not */
_enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
progressSaved =
enrollState
.filterIsInstance<FingerEnrollState.EnrollProgress>()
.filterNotNull()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
}
/** The lottie that should be shown for UDFPS Enrollment */
@@ -272,6 +331,7 @@ class UdfpsViewModel(
private val orientationInteractor: OrientationInteractor,
private val backgroundViewModel: BackgroundViewModel,
private val sensorRepository: FingerprintSensorRepository,
private val udfpsEnrollInteractor: UdfpsEnrollInteractor,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@@ -287,6 +347,7 @@ class UdfpsViewModel(
orientationInteractor,
backgroundViewModel,
sensorRepository,
udfpsEnrollInteractor,
)
as T
}

View File

@@ -1,89 +0,0 @@
/*
* 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.lib.model.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>
/** The current index of [guidedEnrollmentPoints] for the guided enrollment. */
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
}
val 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
}
}

View File

@@ -24,6 +24,7 @@ import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
@@ -37,7 +38,6 @@ 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.lib.model.StageViewModel
import kotlin.math.sin
/**
@@ -51,11 +51,11 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
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
private var guidedEnrollmentOffset: PointF? = null
/**
* This is the physical location of the sensor. This rect will be updated by [drawSensorRectAt]
@@ -143,45 +143,6 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
invalidateSelf()
}
/** Update the progress of the icon */
fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) {
restoreAnimationTime()
// If we are restoring this view from a saved state, set animation duration to 0 to avoid
// animating progress that has already occurred.
if (isRecreating) {
setAnimationTimeToZero()
} else {
restoreAnimationTime()
}
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)
val shouldAnimateMovement =
!currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f
if (shouldAnimateMovement) {
targetAnimatorSet?.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()
}
/** Stop drawing the fingerprint icon. */
fun stopDrawing() {
alpha = 0
@@ -211,6 +172,7 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
if (currentBounds.equals(offsetRect)) {
return
}
val xAnimator = ValueAnimator.ofFloat(currentBounds.left.toFloat(), offsetRect.left)
xAnimator.addUpdateListener {
currX = it.animatedValue as Float
@@ -260,6 +222,40 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
targetAnimationDuration = TARGET_ANIM_DURATION_LONG
}
/**
* Indicates a change to guided enrollment has occurred. Also indicates if we are recreating the
* view, in which case their is no need to animate the icon to whatever position it was in.
*/
fun updateGuidedEnrollment(point: PointF, isRecreating: Boolean) {
guidedEnrollmentOffset = point
if (isRecreating) {
setAnimationTimeToZero()
} else {
restoreAnimationTime()
}
val currentBounds = getCurrLocation().toRect()
val offset = guidedEnrollmentOffset
if (offset?.x != 0f && offset?.y != 0f) {
val targetRect = Rect(sensorRectBounds).toRectF()
// 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.
targetRect.offset(offset!!.x, offset!!.y)
val shouldAnimateMovement = !currentBounds.equals(targetRect)
if (shouldAnimateMovement) {
targetAnimatorSet?.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)
}
} else {
// If we are not offsetting the sensor, move it back to its original place
animateMovement(currentBounds, sensorRectBounds.toRectF(), false)
}
invalidateSelf()
}
companion object {
private const val TAG = "UdfpsEnrollDrawableV2"
private const val DEFAULT_STROKE_WIDTH = 3f

View File

@@ -27,10 +27,12 @@ import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.util.Log
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import android.view.animation.OvershootInterpolator
import androidx.annotation.ColorInt
import androidx.core.animation.addListener
import androidx.core.animation.doOnEnd
import androidx.core.graphics.toRectF
import com.android.internal.annotations.VisibleForTesting
@@ -46,6 +48,7 @@ import kotlin.math.sin
class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: AttributeSet?) :
Drawable() {
private val sensorRect: Rect = Rect()
private var onFinishedCompletionAnimation: (() -> Unit)? = null
private var rotation: Int = 0
private val strokeWidthPx: Float
@@ -287,6 +290,12 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr
checkMarkDrawable.bounds = newBounds
checkMarkDrawable.setVisible(true, false)
}
doOnEnd {
onFinishedCompletionAnimation?.let{
it()
}
}
start()
}
}
@@ -380,6 +389,13 @@ class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: Attr
checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
}
/**
* Indicates that the finish animation has completed, and enrollment can proceed to the next stage
*/
fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) {
this.onFinishedCompletionAnimation = onFinishedAnimation
}
companion object {
private const val TAG = "UdfpsProgressBar"
private const val FILL_COLOR_ANIMATION_DURATION_MS = 350L

View File

@@ -18,6 +18,7 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrol
import android.content.Context
import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
@@ -31,7 +32,6 @@ import android.widget.FrameLayout
import android.widget.ImageView
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -53,6 +53,13 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
private val udfpsUtils: UdfpsUtils = UdfpsUtils()
private lateinit var touchExplorationAnnouncer: TouchExplorationAnnouncer
private var isRecreating = false
private var onFinishedCompletionAnimation: (() -> Unit)? = null
init {
fingerprintProgressDrawable.setFinishAnimationCompleted {
onFinishedCompletionAnimation?.let { it() }
}
}
/**
* This function computes the center (x,y) location with respect to the parent [FrameLayout] for
@@ -112,11 +119,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils)
}
/** Updates the current enrollment stage. */
fun updateStage(it: StageViewModel) {
fingerprintIcon.updateStage(it)
}
/** Receive enroll progress event */
fun onUdfpsEvent(event: FingerEnrollState) {
when (event) {
@@ -174,7 +176,6 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
/** Receive enroll progress event */
private fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
fingerprintIcon.onEnrollmentProgress(remaining, totalSteps)
fingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps)
}
@@ -241,10 +242,25 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
/** Indicates we should should restore the views saved state. */
fun onEnrollProgressSaved(it: FingerEnrollState.EnrollProgress) {
fingerprintIcon.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true)
fingerprintProgressDrawable.onEnrollmentProgress(it.remainingSteps, it.totalStepsRequired, true)
}
/** Indicates we are recreating the UI from a saved state. */
fun onGuidedPointSaved(it: PointF) {
fingerprintIcon.updateGuidedEnrollment(it, true)
}
/**
* Indicates that the finish animation has completed, and enrollment can proceed to the next stage
*/
fun setFinishAnimationCompleted(onFinishedAnimation: () -> Unit) {
this.onFinishedCompletionAnimation = onFinishedAnimation
}
fun updateGuidedEnrollment(point: PointF) {
fingerprintIcon.updateGuidedEnrollment(point, false)
}
companion object {
private const val TAG = "UdfpsEnrollView"
}

View File

@@ -88,7 +88,10 @@ sealed interface FingerprintNavigationStep {
}
/** UiSteps should have a 1 to 1 mapping between each screen of FingerprintEnrollment */
sealed class UiStep : FingerprintNavigationStep
sealed class UiStep(
val enterTransition: Transition = Transition.EnterFromRight,
val exitTransition: Transition = Transition.ExitToLeft,
) : FingerprintNavigationStep
/** This is the landing page for enrollment, where no content is shown. */
data object Init : UiStep() {
@@ -103,7 +106,7 @@ sealed interface FingerprintNavigationStep {
} else if (state.flowType is FastEnroll) {
TransitionStep(Enrollment(state.fingerprintSensor!!))
} else {
TransitionStep(Introduction)
TransitionStep(Introduction())
}
}
else -> null
@@ -118,7 +121,7 @@ sealed interface FingerprintNavigationStep {
action: FingerprintAction,
): FingerprintNavigationStep? {
return when (action) {
FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction)
FingerprintAction.CONFIRM_DEVICE_SUCCESS -> TransitionStep(Introduction())
FingerprintAction.CONFIRM_DEVICE_FAIL -> Finish(null)
else -> null
}
@@ -126,7 +129,10 @@ sealed interface FingerprintNavigationStep {
}
/** Indicates the FingerprintIntroduction screen is being presented to the user */
data object Introduction : UiStep() {
class Introduction(
enterTransition: Transition = Transition.EnterFromRight,
exitTransition: Transition = Transition.ExitToLeft,
) : UiStep(enterTransition, exitTransition) {
override fun update(
state: NavigationState,
action: FingerprintAction,
@@ -141,7 +147,11 @@ sealed interface FingerprintNavigationStep {
}
/** Indicates the FingerprintEducation screen is being presented to the user */
data class Education(val sensor: FingerprintSensor) : UiStep() {
class Education(
val sensor: FingerprintSensor,
enterTransition: Transition = Transition.EnterFromRight,
exitTransition: Transition = Transition.ExitToLeft,
) : UiStep(enterTransition, exitTransition) {
override fun update(
state: NavigationState,
action: FingerprintAction,
@@ -149,7 +159,8 @@ sealed interface FingerprintNavigationStep {
return when (action) {
FingerprintAction.NEXT -> TransitionStep(Enrollment(state.fingerprintSensor!!))
FingerprintAction.NEGATIVE_BUTTON_PRESSED,
FingerprintAction.PREV -> TransitionStep(Introduction)
FingerprintAction.PREV ->
TransitionStep(Introduction(Transition.EnterFromLeft, Transition.ExitToRight))
else -> null
}
}
@@ -179,7 +190,10 @@ sealed interface FingerprintNavigationStep {
): FingerprintNavigationStep? {
return when (action) {
FingerprintAction.NEXT -> Finish(null)
FingerprintAction.PREV -> TransitionStep(Education(state.fingerprintSensor!!))
FingerprintAction.PREV ->
TransitionStep(
Education(state.fingerprintSensor!!, Transition.EnterFromLeft, Transition.ExitToRight)
)
FingerprintAction.ADD_ANOTHER -> TransitionStep(Enrollment(state.fingerprintSensor!!))
else -> null
}

View File

@@ -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.viewmodel
/** Indicates the type of transitions that can occur between fragments */
sealed class Transition {
/**
* Indicates the new fragment should slide in from the left side
*/
data object EnterFromLeft : Transition()
/**
* Indicates the new fragment should slide in from the right side
*/
data object EnterFromRight : Transition()
/**
* Indicates the old fragment should slide out to the left side
*/
data object ExitToLeft : Transition()
/**
* Indicates the old fragment should slide out to the right side
*/
data object ExitToRight : Transition()
}

View File

@@ -90,7 +90,7 @@ class FingerprintEnrollIntroFragmentTest {
private val navigationViewModel =
FingerprintNavigationViewModel(
Introduction,
Introduction(),
false,
flowViewModel,
interactor

View File

@@ -28,7 +28,7 @@ import platform.test.screenshot.ViewScreenshotTestRule.Mode
@RunWith(AndroidJUnit4::class)
class FingerprintEnrollIntroScreenshotTest {
private val injector: Injector = Injector(FingerprintNavigationStep.Introduction)
private val injector: Injector = Injector(FingerprintNavigationStep.Introduction())
@Rule
@JvmField