UDFPS Enrollment Refactor (4/N)

Accessibility + text/dpi change + rotation should be properly handled.
Debug repos were added to make UI developemnt for UDFPS much easier(not
requiring calls to fingerprint manager).

Change-Id: I89900cea0d9e953124781cdf308fb38858de5d16
This commit is contained in:
Joshua McCloskey
2024-03-18 23:23:43 +00:00
parent 1eca5e767d
commit 0336781be0
41 changed files with 1816 additions and 584 deletions

View File

@@ -2788,6 +2788,7 @@
<activity android:name=".biometrics.fingerprint2.ui.enrollment.activity.FingerprintEnrollmentV2Activity"
android:exported="true"
android:permission="android.permission.MANAGE_FINGERPRINT"
android:configChanges="density"
android:theme="@style/GlifTheme.Light">
<intent-filter>
<action android:name="android.settings.FINGERPRINT_SETUP" />

View File

@@ -0,0 +1,100 @@
<?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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/udfps_layout"
style="?attr/fingerprint_layout_theme"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<!-- 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"
/>
<LinearLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:orientation="vertical">
<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="wrap_content"
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="0dp"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:scaleType="centerInside"
android:visibility="gone"
app:lottie_autoPlay="true"
app:lottie_loop="true"
app:lottie_speed=".85"
/>
</LinearLayout>
<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"
>
<include layout="@layout/fingerprint_v2_udfps_enroll_view" />
</FrameLayout>
</LinearLayout>

View File

@@ -18,7 +18,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/setup_wizard_layout"
android:id="@+id/udfps_layout"
style="?attr/fingerprint_layout_theme"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -0,0 +1,46 @@
/*
* 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.data.repository
import android.os.Build
/** Indicates if the developer has debugging features enabled. */
interface DebuggingRepository {
/** A function that will return if a build is debuggable */
fun isDebuggingEnabled(): Boolean
/** A function that will return if udfps enrollment should be swapped with debug repos */
fun isUdfpsEnrollmentDebuggingEnabled(): Boolean
}
class DebuggingRepositoryImpl : DebuggingRepository {
/**
* This flag can be flipped by the engineer which should allow for certain debugging features to
* be enabled.
*/
private val isBuildDebuggable = Build.IS_DEBUGGABLE
/** This flag indicates if udfps should use debug repos to supply data to its various views. */
private val udfpsEnrollmentDebugEnabled = true
override fun isDebuggingEnabled(): Boolean {
return isBuildDebuggable
}
override fun isUdfpsEnrollmentDebuggingEnabled(): Boolean {
return isDebuggingEnabled() && udfpsEnrollmentDebugEnabled
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.data.repository
import android.graphics.Point
import kotlinx.coroutines.flow.Flow
/**
* This repository simulates touch events. This is mainly used to debug accessibility and ensure
* that talkback is correct.
*/
interface SimulatedTouchEventsRepository {
/**
* A flow simulating user touches.
*/
val touchExplorationDebug: Flow<Point>
}

View File

@@ -0,0 +1,127 @@
/*
* 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.data.repository
import android.graphics.Point
import android.graphics.Rect
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractor
import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.systemui.biometrics.shared.model.FingerprintSensor
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
/**
* This class is used to simulate enroll data. This has two major use cases. 1). Ease of Development
* 2). Bug Fixes
*/
class UdfpsEnrollDebugRepositoryImpl :
FingerprintEnrollInteractor, FingerprintSensorRepository, SimulatedTouchEventsRepository {
override suspend fun enroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason) = flow {
emit(FingerEnrollState.OverlayShown)
delay(200)
emit(FingerEnrollState.EnrollHelp(helpMsgId, "Hello world"))
delay(200)
emit(FingerEnrollState.EnrollProgress(15, 16))
delay(300)
emit(FingerEnrollState.EnrollHelp(helpMsgId, "Hello world"))
delay(1000)
emit(FingerEnrollState.EnrollProgress(14, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(13, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(12, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(11, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(10, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(9, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(8, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(7, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(6, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(5, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(4, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(3, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(2, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(1, 16))
delay(500)
emit(FingerEnrollState.EnrollProgress(0, 16))
}
/** Provides touch events to the UdfpsEnrollFragment */
override val touchExplorationDebug: Flow<Point> = flow {
delay(2000)
emit(pointToLeftOfSensor(sensorRect))
delay(2000)
emit(pointBelowSensor(sensorRect))
delay(2000)
emit(pointToRightOfSensor(sensorRect))
delay(2000)
emit(pointAboveSensor(sensorRect))
}
override val fingerprintSensor: Flow<FingerprintSensor> = flowOf(sensorProps)
private fun pointToLeftOfSensor(sensorLocation: Rect) =
Point(sensorLocation.right + 5, sensorLocation.centerY())
private fun pointToRightOfSensor(sensorLocation: Rect) =
Point(sensorLocation.left - 5, sensorLocation.centerY())
private fun pointBelowSensor(sensorLocation: Rect) =
Point(sensorLocation.centerX(), sensorLocation.bottom + 5)
private fun pointAboveSensor(sensorLocation: Rect) =
Point(sensorLocation.centerX(), sensorLocation.top - 5)
companion object {
private val helpMsgId: Int = 1
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,
)
val sensorProps =
FingerprintSensor(
1,
SensorStrength.STRONG,
5,
FingerprintSensorType.UDFPS_OPTICAL,
sensorRect,
sensorRadius,
)
}
}

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.domain.interactor
import com.android.settings.biometrics.fingerprint2.data.repository.DebuggingRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/** Interactor indicating if certain debug flows are enabled. */
interface DebuggingInteractor {
/** This indicates that certain debug flows are enabled. */
val debuggingEnabled: Flow<Boolean>
/** This indicates if udfps should instead use debug repos to supply data to its various views. */
val udfpsEnrollmentDebuggingEnabled: Flow<Boolean>
}
/**
* This interactor essentially forwards the [DebuggingRepository]
*/
class DebuggingInteractorImpl(val debuggingRepository: DebuggingRepository) : DebuggingInteractor {
override val debuggingEnabled: Flow<Boolean> = flow {
emit(debuggingRepository.isDebuggingEnabled())
}
override val udfpsEnrollmentDebuggingEnabled: Flow<Boolean> = flow {
emit(debuggingRepository.isUdfpsEnrollmentDebuggingEnabled())
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update
/**
* This class is responsible for handling updates to fontScale and displayDensity and forwarding
* these events to classes that need them
*/
interface DisplayDensityInteractor {
/** Indicates the display density has been updated. */
fun updateDisplayDensity(density: Int)
/** Indicates the font scale has been updates. */
fun updateFontScale(fontScale: Float)
/** A flow that propagates fontscale. */
val fontScale: Flow<Float>
/** A flow that propagates displayDensity. */
val displayDensity: Flow<Int>
/** A flow that propagates the default display density. */
val defaultDisplayDensity: Flow<Int>
}
/**
* Implementation of the [DisplayDensityInteractor]. This interactor is used to forward activity
* information to the rest of the application.
*/
class DisplayDensityInteractorImpl(
currentFontScale: Float,
currentDisplayDensity: Int,
defaultDisplayDensity: Int,
scope: CoroutineScope,
) : DisplayDensityInteractor {
override fun updateDisplayDensity(density: Int) {
_displayDensity.update { density }
}
override fun updateFontScale(fontScale: Float) {
_fontScale.update { fontScale }
}
private val _fontScale = MutableStateFlow(currentFontScale)
private val _displayDensity = MutableStateFlow(currentDisplayDensity)
override val fontScale: Flow<Float> = _fontScale.asStateFlow()
override val displayDensity: Flow<Int> = _displayDensity.asStateFlow()
override val defaultDisplayDensity: Flow<Int> =
flowOf(defaultDisplayDensity).shareIn(scope, SharingStarted.Eagerly, 1)
}

View File

@@ -0,0 +1,43 @@
/*
* 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 com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
typealias EnrollStageThresholds = Map<Float, StageViewModel>
/** Interactor that provides enroll stages for enrollment. */
interface EnrollStageInteractor {
/** Provides enroll stages for enrollment. */
val enrollStageThresholds: Flow<EnrollStageThresholds>
}
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,
)
)
}

View File

@@ -0,0 +1,134 @@
/*
* 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.content.Context
import android.hardware.fingerprint.FingerprintEnrollOptions
import android.hardware.fingerprint.FingerprintManager
import android.os.CancellationSignal
import android.util.Log
import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError
import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason
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.FingerprintFlow
import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.update
/** This repository is responsible for collecting all state related to the enroll API. */
interface FingerprintEnrollInteractor {
/**
* By calling this function, [fingerEnrollState] will begin to be populated with data on success.
*/
suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
): Flow<FingerEnrollState>
}
class FingerprintEnrollInteractorImpl(
private val applicationContext: Context,
private val fingerprintEnrollOptions: FingerprintEnrollOptions,
private val fingerprintManager: FingerprintManager,
private val fingerprintFlow: FingerprintFlow,
) : FingerprintEnrollInteractor {
private val enrollRequestOutstanding = MutableStateFlow(false)
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
): Flow<FingerEnrollState> = callbackFlow {
// TODO (b/308456120) Improve this logic
if (enrollRequestOutstanding.value) {
Log.d(TAG, "Outstanding enroll request, waiting 150ms")
delay(150)
if (enrollRequestOutstanding.value) {
Log.e(TAG, "Request still present, continuing")
}
}
enrollRequestOutstanding.update { true }
var streamEnded = false
var totalSteps: Int? = null
val enrollmentCallback =
object : FingerprintManager.EnrollmentCallback() {
override fun onEnrollmentProgress(remaining: Int) {
// This is sort of an implementation detail, but unfortunately the API isn't
// very expressive. If anything we should look at changing the FingerprintManager API.
if (totalSteps == null) {
totalSteps = remaining + 1
}
trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure { error ->
Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error")
}
if (remaining == 0) {
streamEnded = true
enrollRequestOutstanding.update { false }
}
}
override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) {
trySend(FingerEnrollState.EnrollHelp(helpMsgId, helpString.toString())).onFailure { error
->
Log.d(TAG, "onEnrollmentHelp failed to send, due to $error")
}
}
override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) {
trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard)).onFailure { error ->
Log.d(TAG, "onEnrollmentError failed to send, due to $error")
}
Log.d(TAG, "onEnrollmentError($errMsgId)")
streamEnded = true
enrollRequestOutstanding.update { false }
}
}
val cancellationSignal = CancellationSignal()
fingerprintManager.enroll(
hardwareAuthToken,
cancellationSignal,
applicationContext.userId,
enrollmentCallback,
enrollReason.toOriginalReason(),
fingerprintEnrollOptions,
)
awaitClose {
// If the stream has not been ended, and the user has stopped collecting the flow
// before it was over, send cancel.
if (!streamEnded) {
Log.e(TAG, "Cancel is sent from settings for enroll()")
cancellationSignal.cancel()
}
}
}
companion object {
private const val TAG = "FingerprintEnrollStateRepository"
}
}

View File

@@ -18,43 +18,25 @@ package com.android.settings.biometrics.fingerprint2.domain.interactor
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
import com.android.settings.biometrics.BiometricUtils
import com.android.settings.biometrics.fingerprint2.conversion.Util.toEnrollError
import com.android.settings.biometrics.fingerprint2.conversion.Util.toOriginalReason
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
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
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintFlow
import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
import com.android.settings.password.ChooseLockSettingsHelper
import com.android.systemui.biometrics.shared.model.toFingerprintSensor
import com.google.android.setupcompat.util.WizardManagerHelper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.onFailure
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
@@ -66,9 +48,7 @@ class FingerprintManagerInteractorImpl(
private val fingerprintManager: FingerprintManager,
fingerprintSensorRepository: FingerprintSensorRepository,
private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
private val pressToAuthInteractor: PressToAuthInteractor,
private val fingerprintFlow: FingerprintFlow,
private val intent: Intent,
private val fingerprintEnrollStateRepository: FingerprintEnrollInteractor,
) : FingerprintManagerInteractor {
private val maxFingerprints =
@@ -77,7 +57,6 @@ class FingerprintManagerInteractorImpl(
)
private val applicationContext = applicationContext.applicationContext
private val enrollRequestOutstanding = MutableStateFlow(false)
override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair<Long, ByteArray> =
suspendCoroutine {
@@ -113,85 +92,8 @@ class FingerprintManagerInteractorImpl(
override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
override suspend fun enroll(
hardwareAuthToken: ByteArray?,
enrollReason: EnrollReason,
): Flow<FingerEnrollState> = callbackFlow {
// TODO (b/308456120) Improve this logic
if (enrollRequestOutstanding.value) {
Log.d(TAG, "Outstanding enroll request, waiting 150ms")
delay(150)
if (enrollRequestOutstanding.value) {
Log.e(TAG, "Request still present, continuing")
}
}
enrollRequestOutstanding.update { true }
var streamEnded = false
var totalSteps: Int? = null
val enrollmentCallback =
object : FingerprintManager.EnrollmentCallback() {
override fun onEnrollmentProgress(remaining: Int) {
// This is sort of an implementation detail, but unfortunately the API isn't
// very expressive. If anything we should look at changing the FingerprintManager API.
if (totalSteps == null) {
totalSteps = remaining + 1
}
trySend(FingerEnrollState.EnrollProgress(remaining, totalSteps!!)).onFailure { error ->
Log.d(TAG, "onEnrollmentProgress($remaining) failed to send, due to $error")
}
if (remaining == 0) {
streamEnded = true
enrollRequestOutstanding.update { false }
}
}
override fun onEnrollmentHelp(helpMsgId: Int, helpString: CharSequence?) {
trySend(FingerEnrollState.EnrollHelp(helpMsgId, helpString.toString())).onFailure { error
->
Log.d(TAG, "onEnrollmentHelp failed to send, due to $error")
}
}
override fun onEnrollmentError(errMsgId: Int, errString: CharSequence?) {
trySend(errMsgId.toEnrollError(fingerprintFlow == SetupWizard)).onFailure { error ->
Log.d(TAG, "onEnrollmentError failed to send, due to $error")
}
Log.d(TAG, "onEnrollmentError($errMsgId)")
streamEnded = true
enrollRequestOutstanding.update { false }
}
}
val cancellationSignal = CancellationSignal()
if (intent.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1) === -1) {
val isSuw: Boolean = WizardManagerHelper.isAnySetupWizard(intent)
intent.putExtra(BiometricUtils.EXTRA_ENROLL_REASON,
if (isSuw) FingerprintEnrollOptions.ENROLL_REASON_SUW else
FingerprintEnrollOptions.ENROLL_REASON_SETTINGS)
}
fingerprintManager.enroll(
hardwareAuthToken,
cancellationSignal,
applicationContext.userId,
enrollmentCallback,
enrollReason.toOriginalReason(),
toFingerprintEnrollOptions(intent)
)
awaitClose {
// If the stream has not been ended, and the user has stopped collecting the flow
// before it was over, send cancel.
if (!streamEnded) {
Log.e(TAG, "Cancel is sent from settings for enroll()")
cancellationSignal.cancel()
}
}
}
override suspend fun enroll(hardwareAuthToken: ByteArray?, enrollReason: EnrollReason): Flow<FingerEnrollState> =
fingerprintEnrollStateRepository.enroll(hardwareAuthToken, enrollReason)
override suspend fun removeFingerprint(fp: FingerprintData): Boolean = suspendCoroutine {
val callback =
@@ -263,14 +165,4 @@ class FingerprintManagerInteractorImpl(
)
}
private fun toFingerprintEnrollOptions(intent: Intent): FingerprintEnrollOptions {
val reason: Int =
intent.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1)
val builder: FingerprintEnrollOptions.Builder = FingerprintEnrollOptions.Builder()
builder.setEnrollReason(FingerprintEnrollOptions.ENROLL_REASON_UNKNOWN)
if (reason != -1) {
builder.setEnrollReason(reason)
}
return builder.build()
}
}

View File

@@ -17,6 +17,7 @@
package com.android.settings.biometrics.fingerprint2.domain.interactor
import android.content.Context
import android.util.Log
import android.view.OrientationEventListener
import com.android.internal.R
import kotlinx.coroutines.CoroutineScope
@@ -24,16 +25,23 @@ import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transform
/**
* Interactor which provides information about orientation
*/
/** Interactor which provides information about orientation */
interface OrientationInteractor {
/** A flow that contains the information about the orientation changing */
val orientation: Flow<Int>
/** A flow that contains the rotation info */
/**
* A flow that contains the rotation info
*/
val rotation: Flow<Int>
/**
* 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]
@@ -53,24 +61,11 @@ class OrientationInteractorImpl(private val context: Context, activityScope: Cor
}
orientationEventListener.enable()
awaitClose { orientationEventListener.disable() }
}
}.shareIn(activityScope, SharingStarted.Eagerly, replay = 1)
override val rotation: Flow<Int> =
callbackFlow {
val orientationEventListener =
object : OrientationEventListener(context) {
override fun onOrientationChanged(orientation: Int) {
trySend(getRotationFromDefault(context.display!!.rotation))
}
}
orientationEventListener.enable()
awaitClose { orientationEventListener.disable() }
}
.stateIn(
activityScope, // This is tied to the activity scope
SharingStarted.WhileSubscribed(), // When no longer subscribed, we removeTheListener
context.display!!.rotation,
)
override val rotation: Flow<Int> = orientation.transform { emit(context.display!!.rotation) }
override val rotationFromDefault: Flow<Int> = rotation.map { getRotationFromDefault(it) }
override fun getRotationFromDefault(rotation: Int): Int {
val isReverseDefaultRotation =
@@ -81,4 +76,4 @@ class OrientationInteractorImpl(private val context: Context, activityScope: Cor
rotation
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.content.Context
import android.os.Process
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.Vibrator
/** Indicates the possible vibration effects for fingerprint enrollment */
sealed class FingerprintVibrationEffects {
/** A vibration indicating an error */
data object UdfpsError : FingerprintVibrationEffects()
/**
* A vibration indicating success, this usually occurs when progress on the UDFPS enrollment has
* been made
*/
data object UdfpsSuccess : FingerprintVibrationEffects()
/** This vibration typically occurs when a help message is shown during UDFPS enrollment */
data object UdfpsHelp : FingerprintVibrationEffects()
}
/** Interface for sending haptic feedback */
interface VibrationInteractor {
/** This will send a haptic vibration */
fun vibrate(effect: FingerprintVibrationEffects, caller: String)
}
/** Implementation of the VibrationInteractor interface */
class VibrationInteractorImpl(val vibrator: Vibrator, val applicationContext: Context) :
VibrationInteractor {
override fun vibrate(effect: FingerprintVibrationEffects, caller: String) {
val callerString = "$caller::$effect"
val res =
when (effect) {
FingerprintVibrationEffects.UdfpsHelp,
FingerprintVibrationEffects.UdfpsError ->
Pair(VIBRATE_EFFECT_ERROR, FINGERPRINT_ENROLLING_SONIFICATION_ATTRIBUTES)
FingerprintVibrationEffects.UdfpsSuccess ->
Pair(VIBRATE_EFFECT_SUCCESS, HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES)
}
vibrator.vibrate(
Process.myUid(),
applicationContext.opPackageName,
res.first,
callerString,
res.second,
)
}
companion object {
private val VIBRATE_EFFECT_ERROR = VibrationEffect.createWaveform(longArrayOf(0, 5, 55, 60), -1)
private val FINGERPRINT_ENROLLING_SONIFICATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY)
private val HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK)
private val VIBRATE_EFFECT_SUCCESS = VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
}
}

View File

@@ -57,8 +57,7 @@ interface FingerprintManagerInteractor {
/**
* Runs [FingerprintManager.enroll] with the [hardwareAuthToken] and [EnrollReason] for this
* enrollment. Returning the [FingerEnrollState] that represents this fingerprint enrollment
* state.
* enrollment. If successful data in the [fingerprintEnrollState] should be populated.
*/
suspend fun enroll(
hardwareAuthToken: ByteArray?,

View File

@@ -42,4 +42,16 @@ sealed class FingerEnrollState {
val shouldRetryEnrollment: Boolean,
val isCancelled: Boolean,
) : FingerEnrollState()
/** Indicates an acquired event has occurred */
data class Acquired(val acquiredGood: Boolean) : FingerEnrollState()
/** Indicates a pointer down event has occurred */
data object PointerDown : FingerEnrollState()
/** Indicates a pointer up event has occurred */
data object PointerUp : FingerEnrollState()
/** Indicates the overlay has shown */
data object OverlayShown : FingerEnrollState()
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
package com.android.settings.biometrics.fingerprint2.lib.model
/**
* A view model that describes the various stages of UDFPS Enrollment. This stages typically update

View File

@@ -19,8 +19,10 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.activity
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.hardware.fingerprint.FingerprintEnrollOptions
import android.hardware.fingerprint.FingerprintManager
import android.os.Bundle
import android.os.Vibrator
import android.util.Log
import android.view.accessibility.AccessibilityManager
import androidx.activity.result.contract.ActivityResultContracts
@@ -35,21 +37,35 @@ import com.android.settings.Utils.SETTINGS_PACKAGE_NAME
import com.android.settings.biometrics.BiometricEnrollBase
import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
import com.android.settings.biometrics.BiometricUtils
import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint2.data.repository.DebuggingRepositoryImpl
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl
import com.android.settings.biometrics.fingerprint2.data.repository.UdfpsEnrollDebugRepositoryImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.AccessibilityInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.DebuggingInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.DisplayDensityInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractor
import com.android.settings.biometrics.fingerprint2.domain.interactor.EnrollStageInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.FoldStateInteractor
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.PressToAuthInteractorImpl
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
import com.android.settings.biometrics.fingerprint2.lib.model.Settings
import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollConfirmationV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollEnrollingV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollIntroV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.RfpsEnrollFindSensorFragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.SfpsEnrollFindSensorFragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.UdfpsEnrollFindSensorFragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.util.toFingerprintEnrollOptions
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.fragment.RFPSEnrollFragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.viewmodel.RFPSViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.fragment.UdfpsEnrollFragment
@@ -77,6 +93,7 @@ import com.android.settings.flags.Flags
import com.android.settings.password.ChooseLockGeneric
import com.android.settings.password.ChooseLockSettingsHelper
import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
import com.android.settingslib.display.DisplayDensityUtils
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.google.android.setupcompat.util.WizardManagerHelper
import com.google.android.setupdesign.util.ThemeHelper
@@ -95,14 +112,17 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
private lateinit var navigationViewModel: FingerprintNavigationViewModel
private lateinit var gatekeeperViewModel: FingerprintGatekeeperViewModel
private lateinit var fingerprintEnrollViewModel: FingerprintEnrollViewModel
private lateinit var vibrationInteractor: VibrationInteractor
private lateinit var foldStateInteractor: FoldStateInteractor
private lateinit var orientationInteractor: OrientationInteractor
private lateinit var displayDensityInteractor: DisplayDensityInteractor
private lateinit var fingerprintScrollViewModel: FingerprintScrollViewModel
private lateinit var backgroundViewModel: BackgroundViewModel
private lateinit var fingerprintFlowViewModel: FingerprintFlowViewModel
private lateinit var fingerprintEnrollConfirmationViewModel:
FingerprintEnrollConfirmationViewModel
private lateinit var udfpsViewModel: UdfpsViewModel
private lateinit var enrollStageInteractor: EnrollStageInteractor
private val coroutineDispatcher = Dispatchers.Default
/** Result listener for ChooseLock activity flow. */
@@ -135,6 +155,12 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
foldStateInteractor.onConfigurationChange(newConfig)
val displayDensityUtils = DisplayDensityUtils(applicationContext)
val currIndex = displayDensityUtils.currentIndexForDefaultDisplay
displayDensityInteractor.updateFontScale(resources.configuration.fontScale)
displayDensityInteractor.updateDisplayDensity(
displayDensityUtils.defaultDisplayDensityValues[currIndex]
)
}
private fun onConfirmDevice(resultCode: Int, data: Intent?) {
@@ -193,10 +219,43 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
fingerprintFlowViewModel =
ViewModelProvider(this, FingerprintFlowViewModel.FingerprintFlowViewModelFactory(enrollType))[
FingerprintFlowViewModel::class.java]
val displayDensityUtils = DisplayDensityUtils(context)
val currIndex = displayDensityUtils.currentIndexForDefaultDisplay
val defaultDisplayDensity = displayDensityUtils.defaultDensityForDefaultDisplay
displayDensityInteractor =
DisplayDensityInteractorImpl(
resources.configuration.fontScale,
displayDensityUtils.defaultDisplayDensityValues[currIndex],
defaultDisplayDensity,
lifecycleScope,
)
val debuggingRepo = DebuggingRepositoryImpl()
val debuggingInteractor = DebuggingInteractorImpl(debuggingRepo)
val udfpsEnrollDebugRepositoryImpl = UdfpsEnrollDebugRepositoryImpl()
val fingerprintSensorRepo =
FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope)
val pressToAuthInteractor = PressToAuthInteractorImpl(context, backgroundDispatcher)
if (debuggingRepo.isUdfpsEnrollmentDebuggingEnabled()) udfpsEnrollDebugRepositoryImpl
else FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope)
if (intent.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1) === -1) {
val isSuw: Boolean = WizardManagerHelper.isAnySetupWizard(intent)
intent.putExtra(
BiometricUtils.EXTRA_ENROLL_REASON,
if (isSuw) FingerprintEnrollOptions.ENROLL_REASON_SUW
else FingerprintEnrollOptions.ENROLL_REASON_SETTINGS,
)
}
val fingerprintEnrollStateRepository =
if (debuggingRepo.isUdfpsEnrollmentDebuggingEnabled()) udfpsEnrollDebugRepositoryImpl
else
FingerprintEnrollInteractorImpl(
context.applicationContext,
intent.toFingerprintEnrollOptions(),
fingerprintManager,
Settings,
)
val fingerprintManagerInteractor =
FingerprintManagerInteractorImpl(
@@ -205,12 +264,10 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
fingerprintManager,
fingerprintSensorRepo,
GatekeeperPasswordProvider(LockPatternUtils(context)),
pressToAuthInteractor,
enrollType,
getIntent(),
fingerprintEnrollStateRepository,
)
var challenge: Long? = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
var challenge = intent.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
val gatekeeperInfo = FingerprintGatekeeperViewModel.toGateKeeperInfo(challenge, token)
@@ -256,6 +313,8 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
foldStateInteractor.onConfigurationChange(resources.configuration)
orientationInteractor = OrientationInteractorImpl(context, lifecycleScope)
vibrationInteractor =
VibrationInteractorImpl(context.getSystemService(Vibrator::class.java)!!, context)
// Initialize FingerprintViewModel
fingerprintEnrollViewModel =
@@ -309,10 +368,23 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
),
)[RFPSViewModel::class.java]
enrollStageInteractor = EnrollStageInteractorImpl()
udfpsViewModel =
ViewModelProvider(
this,
UdfpsViewModel.UdfpsEnrollmentFactory(),
UdfpsViewModel.UdfpsEnrollmentFactory(
vibrationInteractor,
displayDensityInteractor,
navigationViewModel,
debuggingInteractor,
fingerprintEnrollEnrollingViewModel,
udfpsEnrollDebugRepositoryImpl,
enrollStageInteractor,
orientationInteractor,
backgroundViewModel,
fingerprintSensorRepo,
),
)[UdfpsViewModel::class.java]
fingerprintEnrollConfirmationViewModel =
@@ -348,7 +420,12 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
when (step) {
Confirmation -> FingerprintEnrollConfirmationV2Fragment()
is Education -> {
FingerprintEnrollFindSensorV2Fragment(step.sensor.sensorType)
when (step.sensor.sensorType) {
FingerprintSensorType.REAR -> RfpsEnrollFindSensorFragment()
FingerprintSensorType.UDFPS_OPTICAL,
FingerprintSensorType.UDFPS_ULTRASONIC -> UdfpsEnrollFindSensorFragment()
else -> SfpsEnrollFindSensorFragment()
}
}
is Enrollment -> {
when (step.sensor.sensorType) {
@@ -370,7 +447,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
supportFragmentManager
.beginTransaction()
.setReorderingAllowed(true)
.add(R.id.fragment_container_view, theClass, null)
.add(R.id.fragment_container_view, theClass::class.java, null)
.commit()
navigationViewModel.update(
FingerprintAction.TRANSITION_FINISHED,
@@ -386,7 +463,7 @@ class FingerprintEnrollmentV2Activity : FragmentActivity() {
navigationViewModel.shouldFinish.filterNotNull().collect {
Log.d(TAG, "FingerprintSettingsNav.finishing($it)")
if (it.result != null) {
finishActivity(it.result as Int)
finishActivity(it.result)
} else {
finish()
}

View File

@@ -0,0 +1,132 @@
/*
* 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.fragment.education
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.android.settings.R
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
import com.google.android.setupdesign.GlifLayout
import kotlinx.coroutines.launch
/**
* A fragment that is used to educate the user about the rear fingerprint sensor on this device.
*
* The main goals of this page are
* 1. Inform the user where the fingerprint sensor is on their device
* 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
* will work.
*/
class RfpsEnrollFindSensorFragment() : Fragment() {
/** Used for testing purposes */
private var factory: ViewModelProvider.Factory? = null
@VisibleForTesting
constructor(theFactory: ViewModelProvider.Factory) : this() {
factory = theFactory
}
private val viewModelProvider: ViewModelProvider by lazy {
if (factory != null) {
ViewModelProvider(requireActivity(), factory!!)
} else {
ViewModelProvider(requireActivity())
}
}
private var animation: FingerprintFindSensorAnimation? = null
private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val view =
inflater.inflate(R.layout.fingerprint_v2_enroll_find_sensor, container, false)!! as GlifLayout
view.setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title)
view.setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
// Set up footer bar
val footerBarMixin = view.getMixin(FooterBarMixin::class.java)
setupSecondaryButton(footerBarMixin)
lifecycleScope.launch {
viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
}
lifecycleScope.launch {
viewModel.showRfpsAnimation.collect {
animation = view.findViewById(R.id.fingerprint_sensor_location_animation)
animation!!.startAnimation()
}
}
lifecycleScope.launch {
viewModel.showErrorDialog.collect { (errMsgId, isSetup) ->
// TODO: Covert error dialog kotlin as well
FingerprintErrorDialog.showErrorDialog(requireActivity(), errMsgId, isSetup)
}
}
return view
}
override fun onDestroy() {
animation?.stopAnimation()
super.onDestroy()
}
private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) {
footerBarMixin.secondaryButton =
FooterButton.Builder(requireActivity())
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
.setListener { viewModel.secondaryButtonClicked() }
.setButtonType(FooterButton.ButtonType.SKIP)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
.build()
}
private fun setupPrimaryButton(footerBarMixin: FooterBarMixin) {
footerBarMixin.primaryButton =
FooterButton.Builder(requireActivity())
.setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
.setListener {
Log.d(TAG, "onStartButtonClick")
viewModel.proceedToEnrolling()
}
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
.build()
}
companion object {
private const val TAG = "RfpsEnrollFindSensor"
}
}

View File

@@ -0,0 +1,162 @@
/*
* 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.fragment.education
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.airbnb.lottie.LottieAnimationView
import com.android.settings.R
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
import com.google.android.setupdesign.GlifLayout
import kotlinx.coroutines.launch
/**
* A fragment that is used to educate the user about the side fingerprint sensor on this device.
*
* The main goals of this page are
* 1. Inform the user where the fingerprint sensor is on their device
* 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
* will work.
*/
class SfpsEnrollFindSensorFragment() : Fragment() {
/** Used for testing purposes */
private var factory: ViewModelProvider.Factory? = null
@VisibleForTesting
constructor(theFactory: ViewModelProvider.Factory) : this() {
factory = theFactory
}
private val viewModelProvider: ViewModelProvider by lazy {
if (factory != null) {
ViewModelProvider(requireActivity(), factory!!)
} else {
ViewModelProvider(requireActivity())
}
}
private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val view =
inflater.inflate(R.layout.sfps_enroll_find_sensor_layout, container, false)!! as GlifLayout
view.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
view.setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message)
// Set up footer bar
val footerBarMixin = view.getMixin(FooterBarMixin::class.java)
setupSecondaryButton(footerBarMixin)
// Set up lottie
lifecycleScope.launch {
viewModel.sfpsLottieInfo.collect { (isFolded, rotation) ->
setupLottie(view, getSfpsIllustrationLottieAnimation(isFolded, rotation))
}
}
lifecycleScope.launch {
viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
}
lifecycleScope.launch {
viewModel.showErrorDialog.collect { (errMsgId, isSetup) ->
// TODO: Covert error dialog kotlin as well
FingerprintErrorDialog.showErrorDialog(requireActivity(), errMsgId, isSetup)
}
}
return view
}
private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) {
footerBarMixin.secondaryButton =
FooterButton.Builder(requireActivity())
.setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
.setListener { viewModel.secondaryButtonClicked() }
.setButtonType(FooterButton.ButtonType.SKIP)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Secondary)
.build()
}
private fun setupPrimaryButton(footerBarMixin: FooterBarMixin) {
footerBarMixin.primaryButton =
FooterButton.Builder(requireActivity())
.setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
.setListener {
Log.d(TAG, "onStartButtonClick")
viewModel.proceedToEnrolling()
}
.setButtonType(FooterButton.ButtonType.NEXT)
.setTheme(com.google.android.setupdesign.R.style.SudGlifButton_Primary)
.build()
}
private fun getSfpsIllustrationLottieAnimation(isFolded: Boolean, rotation: Int): Int {
val animation: Int
when (rotation) {
Surface.ROTATION_90 ->
animation =
(if (isFolded) R.raw.fingerprint_edu_lottie_folded_top_left
else R.raw.fingerprint_edu_lottie_portrait_top_left)
Surface.ROTATION_180 ->
animation =
(if (isFolded) R.raw.fingerprint_edu_lottie_folded_bottom_left
else R.raw.fingerprint_edu_lottie_landscape_bottom_left)
Surface.ROTATION_270 ->
animation =
(if (isFolded) R.raw.fingerprint_edu_lottie_folded_bottom_right
else R.raw.fingerprint_edu_lottie_portrait_bottom_right)
else ->
animation =
(if (isFolded) R.raw.fingerprint_edu_lottie_folded_top_right
else R.raw.fingerprint_edu_lottie_landscape_top_right)
}
return animation
}
private fun setupLottie(
view: View,
lottieAnimation: Int,
lottieClickListener: View.OnClickListener? = null,
) {
val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
illustrationLottie?.setAnimation(lottieAnimation)
illustrationLottie?.playAnimation()
illustrationLottie?.setOnClickListener(lottieClickListener)
illustrationLottie?.visibility = View.VISIBLE
}
companion object {
private const val TAG = "SfpsEnrollFindSensor"
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment
package com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education
import android.os.Bundle
import android.util.Log
@@ -29,36 +29,27 @@ import androidx.lifecycle.lifecycleScope
import com.airbnb.lottie.LottieAnimationView
import com.android.settings.R
import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog
import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollFindSensorViewModel
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.google.android.setupcompat.template.FooterBarMixin
import com.google.android.setupcompat.template.FooterButton
import com.google.android.setupdesign.GlifLayout
import kotlinx.coroutines.launch
private const val TAG = "FingerprintEnrollFindSensorV2Fragment"
/**
* A fragment that is used to educate the user about the fingerprint sensor on this device.
*
* If the sensor is not a udfps sensor, this fragment listens to fingerprint enrollment for
* proceeding to the enroll enrolling.
* A fragment that is used to educate the user about the under display fingerprint sensor on this
* device.
*
* The main goals of this page are
* 1. Inform the user where the fingerprint sensor is on their device
* 2. Explain to the user how the enrollment process shown by [FingerprintEnrollEnrollingV2Fragment]
* will work.
*/
class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorType) : Fragment() {
class UdfpsEnrollFindSensorFragment() : Fragment() {
/** Used for testing purposes */
private var factory: ViewModelProvider.Factory? = null
@VisibleForTesting
constructor(
sensorType: FingerprintSensorType,
theFactory: ViewModelProvider.Factory,
) : this(sensorType) {
constructor(theFactory: ViewModelProvider.Factory) : this() {
factory = theFactory
}
@@ -70,10 +61,6 @@ class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorTyp
}
}
// This is only for non-udfps or non-sfps sensor. For udfps and sfps, we show lottie.
private var animation: FingerprintFindSensorAnimation? = null
private var contentLayoutId: Int = -1
private val viewModel: FingerprintEnrollFindSensorViewModel by lazy {
viewModelProvider[FingerprintEnrollFindSensorViewModel::class.java]
}
@@ -83,31 +70,18 @@ class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorTyp
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
contentLayoutId =
when (sensorType) {
FingerprintSensorType.UDFPS_OPTICAL,
FingerprintSensorType.UDFPS_ULTRASONIC -> R.layout.udfps_enroll_find_sensor_layout
FingerprintSensorType.POWER_BUTTON -> R.layout.sfps_enroll_find_sensor_layout
else -> R.layout.fingerprint_v2_enroll_find_sensor
}
val view = inflater.inflate(contentLayoutId, container, false)!! as GlifLayout
setTexts(sensorType, view)
val view =
inflater.inflate(R.layout.udfps_enroll_find_sensor_layout, container, false)!! as GlifLayout
view.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title)
view.setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message)
// Set up footer bar
val footerBarMixin = view.getMixin(FooterBarMixin::class.java)
setupSecondaryButton(footerBarMixin)
lifecycleScope.launch {
viewModel.showPrimaryButton.collect { setupPrimaryButton(footerBarMixin) }
}
// Set up lottie or animation
lifecycleScope.launch {
viewModel.sfpsLottieInfo.collect { (isFolded, rotation) ->
setupLottie(view, getSfpsIllustrationLottieAnimation(isFolded, rotation))
}
}
lifecycleScope.launch {
viewModel.udfpsLottieInfo.collect { isAccessibilityEnabled ->
val lottieAnimation =
@@ -115,12 +89,6 @@ class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorTyp
setupLottie(view, lottieAnimation) { viewModel.proceedToEnrolling() }
}
}
lifecycleScope.launch {
viewModel.showRfpsAnimation.collect {
animation = view.findViewById(R.id.fingerprint_sensor_location_animation)
animation!!.startAnimation()
}
}
lifecycleScope.launch {
viewModel.showErrorDialog.collect { (errMsgId, isSetup) ->
@@ -131,11 +99,6 @@ class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorTyp
return view
}
override fun onDestroy() {
animation?.stopAnimation()
super.onDestroy()
}
private fun setupSecondaryButton(footerBarMixin: FooterBarMixin) {
footerBarMixin.secondaryButton =
FooterButton.Builder(requireActivity())
@@ -159,36 +122,6 @@ class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorTyp
.build()
}
private fun setupLottie(
view: View,
lottieAnimation: Int,
lottieClickListener: View.OnClickListener? = null,
) {
val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
illustrationLottie?.setAnimation(lottieAnimation)
illustrationLottie?.playAnimation()
illustrationLottie?.setOnClickListener(lottieClickListener)
illustrationLottie?.visibility = View.VISIBLE
}
private fun setTexts(sensorType: FingerprintSensorType?, view: GlifLayout) {
when (sensorType) {
FingerprintSensorType.UDFPS_OPTICAL,
FingerprintSensorType.UDFPS_ULTRASONIC -> {
view.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title)
view.setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message)
}
FingerprintSensorType.POWER_BUTTON -> {
view.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title)
view.setDescriptionText(R.string.security_settings_sfps_enroll_find_sensor_message)
}
else -> {
view.setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title)
view.setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message)
}
}
}
private fun getSfpsIllustrationLottieAnimation(isFolded: Boolean, rotation: Int): Int {
val animation: Int
when (rotation) {
@@ -211,4 +144,20 @@ class FingerprintEnrollFindSensorV2Fragment(val sensorType: FingerprintSensorTyp
}
return animation
}
private fun setupLottie(
view: View,
lottieAnimation: Int,
lottieClickListener: View.OnClickListener? = null,
) {
val illustrationLottie: LottieAnimationView? = view.findViewById(R.id.illustration_lottie)
illustrationLottie?.setAnimation(lottieAnimation)
illustrationLottie?.playAnimation()
illustrationLottie?.setOnClickListener(lottieClickListener)
illustrationLottie?.visibility = View.VISIBLE
}
companion object {
private const val TAG = "UdfpsEnrollFindSensor"
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.common.util
import android.content.Intent
import android.hardware.fingerprint.FingerprintEnrollOptions
import com.android.settings.biometrics.BiometricUtils
fun Intent.toFingerprintEnrollOptions(): FingerprintEnrollOptions {
val reason: Int = this.getIntExtra(BiometricUtils.EXTRA_ENROLL_REASON, -1)
val builder: FingerprintEnrollOptions.Builder = FingerprintEnrollOptions.Builder()
builder.setEnrollReason(FingerprintEnrollOptions.ENROLL_REASON_UNKNOWN)
if (reason != -1) {
builder.setEnrollReason(reason)
}
return builder.build()
}

View File

@@ -1,5 +1,5 @@
/*
* 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget
import android.app.AlertDialog
import android.app.Dialog
@@ -29,8 +29,6 @@ import com.android.settings.core.instrumentation.InstrumentedDialogFragment
import kotlin.coroutines.resume
import kotlinx.coroutines.suspendCancellableCoroutine
private const val TAG = "FingerprintErrorDialog"
/** A Dialog used for fingerprint enrollment when an error occurs. */
class FingerprintErrorDialog : InstrumentedDialogFragment() {
private lateinit var onContinue: DialogInterface.OnClickListener
@@ -82,6 +80,7 @@ class FingerprintErrorDialog : InstrumentedDialogFragment() {
}
companion object {
private const val TAG = "FingerprintErrorDialog"
private const val KEY_MESSAGE = "fingerprint_message"
private const val KEY_TITLE = "fingerprint_title"
private const val KEY_SHOULD_TRY_AGAIN = "should_try_again"

View File

@@ -34,9 +34,9 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.android.settings.R
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.widget.FingerprintErrorDialog
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
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.FingerprintErrorDialog
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.IconTouchDialog
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.rfps.ui.widget.RFPSProgressBar
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.BackgroundViewModel

View File

@@ -43,7 +43,7 @@ class RFPSViewModel(
orientationInteractor: OrientationInteractor,
) : ViewModel() {
private val _textViewIsVisible = MutableStateFlow<Boolean>(false)
private val _textViewIsVisible = MutableStateFlow(false)
/** Value to indicate if the text view is visible or not */
val textViewIsVisible: Flow<Boolean> = _textViewIsVisible.asStateFlow()
@@ -52,7 +52,7 @@ class RFPSViewModel(
/** Indicates if the icon should be animating or not */
val shouldAnimateIcon = _shouldAnimateIcon
private var enrollFlow: Flow<FingerEnrollState?> = fingerprintEnrollViewModel.enrollFLow
private var enrollFlow: Flow<FingerEnrollState?> = fingerprintEnrollViewModel.enrollFlow
/**
* Enroll progress message with a replay of size 1 allowing for new subscribers to get the most
@@ -142,7 +142,7 @@ class RFPSViewModel(
_textViewIsVisible.update { false }
_shouldAnimateIcon = fingerprintEnrollViewModel.enrollFlowShouldBeRunning
/** Indicates if the icon should be animating or not */
enrollFlow = fingerprintEnrollViewModel.enrollFLow
enrollFlow = fingerprintEnrollViewModel.enrollFlow
}
class RFPSViewModelFactory(

View File

@@ -18,6 +18,8 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrol
import android.os.Bundle
import android.util.Log
import android.view.MotionEvent
import android.view.MotionEvent.ACTION_HOVER_MOVE
import android.view.View
import android.view.WindowManager
import android.widget.TextView
@@ -30,10 +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.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.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.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
@@ -47,6 +51,7 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
private var factory: ViewModelProvider.Factory? = null
private val viewModel: UdfpsViewModel by lazy { viewModelProvider[UdfpsViewModel::class.java] }
private lateinit var udfpsEnrollView: UdfpsEnrollViewV2
private lateinit var lottie: LottieAnimationView
private val viewModelProvider: ViewModelProvider by lazy {
if (factory != null) {
@@ -63,7 +68,8 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val illustrationLottie: LottieAnimationView = view.findViewById(R.id.illustration_lottie)!!
val fragment = this
lottie = 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)!!
@@ -79,6 +85,11 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch {
viewModel.sensorLocation.collect { sensor ->
udfpsEnrollView.setSensorRect(sensor.sensorBounds, sensor.sensorType)
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.headerText.collect { titleTextView.setText(it.toResource()) }
}
@@ -92,35 +103,59 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.sensorLocation.collect { rect -> udfpsEnrollView.setSensorRect(rect) }
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.accessibilityEnabled.collect { isEnabled -> udfpsEnrollView.setAccessibilityEnabled(isEnabled) }
viewModel.shouldShowLottie.collect {
lottie.visibility = if (it) View.VISIBLE else View.GONE
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.lottie.collect { lottieModel ->
if (lottie.visibility == View.GONE) {
return@collect
}
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()
lottie.setComposition(composition)
lottie.visibility = View.VISIBLE
lottie.playAnimation()
}
}
} else {
illustrationLottie.visibility = View.INVISIBLE
lottie.visibility = View.INVISIBLE
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.udfpsEvent.collect {
Log.d(TAG, "EnrollEvent $it")
udfpsEnrollView.onUdfpsEvent(it) }
repeatOnLifecycle(Lifecycle.State.DESTROYED) { viewModel.stopEnrollment() }
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.accessibilityEnabled.collect { enabled ->
udfpsEnrollView.setAccessibilityEnabled(enabled)
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.enrollState.collect {
Log.d(TAG, "EnrollEvent $it")
if (it is FingerEnrollState.EnrollError) {
try {
FingerprintErrorDialog.showInstance(it, fragment)
} catch (exception: Exception) {
Log.e(TAG, "Exception occurred $exception")
}
} else {
udfpsEnrollView.onUdfpsEvent(it)
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.progressSaved.collect { udfpsEnrollView.onEnrollProgressSaved(it) }
}
viewLifecycleOwner.lifecycleScope.launch {
@@ -128,6 +163,15 @@ class UdfpsEnrollFragment() : Fragment(R.layout.fingerprint_v2_udfps_enroll_enro
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.touchExplorationDebug.collect {
udfpsEnrollView.sendDebugTouchExplorationEvent(
MotionEvent.obtain(100, 100, ACTION_HOVER_MOVE, it.x.toFloat(), it.y.toFloat(), 0)
)
}
}
viewModel.readyForEnrollment()
}
private fun HeaderText.toResource(): Int {

View File

@@ -16,6 +16,8 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
/** Represents the description text for UDFPS enrollment */
data class DescriptionText(
val isSuw: Boolean,

View File

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

View File

@@ -16,6 +16,8 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
/** Represents the header text for UDFPS enrollment */
data class HeaderText(
val isSuw: Boolean,

View File

@@ -1,41 +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.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()

View File

@@ -16,167 +16,284 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.viewmodel
import android.graphics.Rect
import android.graphics.Point
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.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.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.viewmodel.BackgroundViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintAction
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintEnrollEnrollingViewModel
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
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
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
/** ViewModel used to drive UDFPS Enrollment through [UdfpsEnrollFragment] */
class UdfpsViewModel() : ViewModel() {
class UdfpsViewModel(
val vibrationInteractor: VibrationInteractor,
displayDensityInteractor: DisplayDensityInteractor,
val navigationViewModel: FingerprintNavigationViewModel,
debuggingInteractor: DebuggingInteractor,
val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
simulatedTouchEventsDebugRepository: SimulatedTouchEventsRepository,
enrollStageInteractor: EnrollStageInteractor,
orientationInteractor: OrientationInteractor,
backgroundViewModel: BackgroundViewModel,
sensorRepository: FingerprintSensorRepository,
) : ViewModel() {
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)
private var _enrollState: Flow<FingerEnrollState?> =
fingerprintEnrollEnrollingViewModel.enrollFlow
/** The current state of the enrollment. */
var enrollState: Flow<FingerEnrollState> =
combine(fingerprintEnrollEnrollingViewModel.enrollFlowShouldBeRunning, _enrollState) {
shouldBeRunning,
state ->
if (shouldBeRunning) {
state
} else {
null
}
}
.filterNotNull()
/**
* Forwards the property sensor information. This is typically used to recreate views that must be
* aligned with the sensor.
*/
val sensorLocation = sensorRepository.fingerprintSensor
/** Indicates if accessibility is enabled */
val accessibilityEnabled = flowOf(true).shareIn(viewModelScope, SharingStarted.Eagerly, 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 {
backgroundViewModel.background.filter { it }.collect { didGoToBackground() }
}
}
/**
* This is the saved progress, this is for when views are recreated and need saved state for the
* first time.
*/
var progressSaved: Flow<FingerEnrollState.EnrollProgress> =
enrollState
.filterIsInstance<FingerEnrollState.EnrollProgress>()
.filterNotNull()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
/** This sends touch exploration events only used for debugging purposes. */
val touchExplorationDebug: Flow<Point> =
debuggingInteractor.debuggingEnabled.combineTransform(
simulatedTouchEventsDebugRepository.touchExplorationDebug
) { enabled, point ->
if (enabled) {
emit(point)
}
}
/** Determines the current [StageViewModel] enrollment is in */
val enrollStage: Flow<StageViewModel> =
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
thresholds.forEach { (threshold, stage) ->
if (progress < threshold) {
return@forEach
}
stageToReturn = stage
}
stageToReturn
} else {
null
}
}
.filterNotNull()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
/** Indicates if we should show the lottie. */
val shouldShowLottie: Flow<Boolean> =
combine(
displayDensityInteractor.displayDensity,
displayDensityInteractor.defaultDisplayDensity,
displayDensityInteractor.fontScale,
orientationInteractor.rotation,
) { currDisplayDensity, defaultDisplayDensity, fontScale, rotation ->
val canShowLottieForRotation =
when (rotation) {
Surface.ROTATION_0 -> true
else -> false
}
canShowLottieForRotation &&
if (fontScale > 1.0f) {
false
} else {
defaultDisplayDensity == currDisplayDensity
}
}
.shareIn(viewModelScope, SharingStarted.Eagerly, 1)
/** The header text for UDFPS enrollment */
val headerText: Flow<HeaderText> =
combine(isSetupWizard, accessibilityEnabled, enrollStage) { isSuw, isAccessibility, stage ->
return@combine HeaderText(isSuw, isAccessibility, stage)
}
return@combine HeaderText(isSuw, isAccessibility, stage)
}
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
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)
isSuw,
isAccessibility,
stage,
shouldClearText ->
if (shouldClearText) {
return@combine null
} else {
return@combine DescriptionText(isSuw, isAccessibility, stage)
}
}
}
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
/** Indicates if the consumer is ready for enrollment */
fun readyForEnrollment() {
fingerprintEnrollEnrollingViewModel.canEnroll()
}
/** Indicates if enrollment should stop */
fun stopEnrollment() {
fingerprintEnrollEnrollingViewModel.stopEnroll()
}
/** Indicates the negative button has been clicked */
fun negativeButtonClicked() {
doReset()
navigationViewModel.update(
FingerprintAction.NEGATIVE_BUTTON_PRESSED,
navStep,
"$TAG#negativeButtonClicked",
)
}
/** Indicates that an enrollment was completed */
fun finishedSuccessfully() {
doReset()
navigationViewModel.update(FingerprintAction.NEXT, navStep, "${TAG}#progressFinished")
}
/** Indicates that the application went to the background. */
private fun didGoToBackground() {
navigationViewModel.update(
FingerprintAction.DID_GO_TO_BACKGROUND,
navStep,
"$TAG#didGoToBackground",
)
stopEnrollment()
}
private fun doReset() {
/** Indicates if the icon should be animating or not */
_enrollState = fingerprintEnrollEnrollingViewModel.enrollFlow
}
/** 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()
return@combine EducationAnimationModel(isSuw, isAccessibility, stage)
}
.distinctUntilChanged()
.shareIn(this.viewModelScope, SharingStarted.Eagerly, replay = 1)
class UdfpsEnrollmentFactory() : ViewModelProvider.Factory {
/** Indicates we should send a vibration event */
private fun vibrate(event: FingerEnrollState) {
val vibrationEvent =
when (event) {
is FingerEnrollState.EnrollError -> FingerprintVibrationEffects.UdfpsError
is FingerEnrollState.EnrollHelp -> FingerprintVibrationEffects.UdfpsHelp
is FingerEnrollState.EnrollProgress -> FingerprintVibrationEffects.UdfpsSuccess
else -> FingerprintVibrationEffects.UdfpsError
}
vibrationInteractor.vibrate(vibrationEvent, "UdfpsEnrollFragment")
}
class UdfpsEnrollmentFactory(
private val vibrationInteractor: VibrationInteractor,
private val displayDensityInteractor: DisplayDensityInteractor,
private val navigationViewModel: FingerprintNavigationViewModel,
private val debuggingInteractor: DebuggingInteractor,
private val fingerprintEnrollEnrollingViewModel: FingerprintEnrollEnrollingViewModel,
private val simulatedTouchEventsRepository: SimulatedTouchEventsRepository,
private val enrollStageInteractor: EnrollStageInteractor,
private val orientationInteractor: OrientationInteractor,
private val backgroundViewModel: BackgroundViewModel,
private val sensorRepository: FingerprintSensorRepository,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return UdfpsViewModel() as T
return UdfpsViewModel(
vibrationInteractor,
displayDensityInteractor,
navigationViewModel,
debuggingInteractor,
fingerprintEnrollEnrollingViewModel,
simulatedTouchEventsRepository,
enrollStageInteractor,
orientationInteractor,
backgroundViewModel,
sensorRepository,
)
as T
}
}
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),
listOf(UdfpsHelp(1,"hi")),
listOf(UdfpsHelp(1,"hi")),
CreateProgress(15, 16),
listOf(UdfpsHelp(1,"hi")),
CreateProgress(14, 16),
listOf(PointerDown, UdfpsHelp(1,"hi"), PointerUp),
listOf(PointerDown, UdfpsHelp(1,"hi"), PointerUp),
CreateProgress(13, 16),
CreateProgress(12, 16),
CreateProgress(11, 16),
CreateProgress(10, 16),
CreateProgress(9, 16),
CreateProgress(8, 16),
CreateProgress(7, 16),
CreateProgress(6, 16),
CreateProgress(5, 16),
CreateProgress(4, 16),
CreateProgress(3, 16),
CreateProgress(2, 16),
CreateProgress(1, 16),
CreateProgress(0, 16),
)
/** This will be removed */
private fun CreateProgress(remaining: Int, total: Int): List<UdfpsEnrollEvent> {
return listOf(PointerDown, Acquired(true), UdfpsProgress(remaining, total), PointerUp)
}
}
}

View File

@@ -20,7 +20,7 @@ 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
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) {
@@ -28,6 +28,7 @@ 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 {
@@ -76,7 +77,7 @@ class UdfpsEnrollHelperV2(private val mContext: Context) {
if (accessibilityEnabled || !isGuidedEnrollment) {
return null
}
var scale = SCALE
val scale = SCALE
val originalPoint = guidedEnrollmentPoints[index % guidedEnrollmentPoints.size]
return PointF(originalPoint.x * scale, originalPoint.y * scale)
}

View File

@@ -37,7 +37,7 @@ 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 com.android.settings.biometrics.fingerprint2.lib.model.StageViewModel
import kotlin.math.sin
/**
@@ -45,6 +45,7 @@ import kotlin.math.sin
* various stages of enrollment
*/
class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeSet?) : Drawable() {
private var targetAnimationDuration: Long = TARGET_ANIM_DURATION_LONG
private var targetAnimatorSet: AnimatorSet? = null
private val movingTargetFpIcon: Drawable
private val fingerprintDrawable: ShapeDrawable
@@ -88,22 +89,25 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
it.recycle()
}
sensorOutlinePaint = Paint(0 /* flags */).apply {
isAntiAlias = true
setColor(movingTargetFill)
style = Paint.Style.FILL
}
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
}
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()
}
movingTargetFpIcon =
context.resources.getDrawable(R.drawable.ic_enrollment_fingerprint, null).apply {
setTint(enrollIconColor)
mutate()
}
fingerprintDrawable.setTint(enrollIconColor)
setAlpha(255)
@@ -140,7 +144,16 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
}
/** Update the progress of the icon */
fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
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()
@@ -149,10 +162,10 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
// 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 =
val shouldAnimateMovement =
!currentBounds.equals(targetRect) && offset.x != 0f && offset.y != 0f
if (shouldAnimateMovement) {
targetAnimatorSet?.let { it.cancel() }
targetAnimatorSet?.cancel()
animateMovement(currentBounds, targetRect, true)
}
} else {
@@ -186,7 +199,7 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
val currLocation = getCurrLocation()
canvas.scale(currentScale, currentScale, currLocation.centerX(), currLocation.centerY())
sensorRectBounds?.let { canvas.drawOval(currLocation, sensorOutlinePaint) }
canvas.drawOval(currLocation, sensorOutlinePaint)
fingerprintDrawable.bounds = currLocation.toRect()
fingerprintDrawable.draw(canvas)
}
@@ -234,6 +247,19 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
}
}
/**
* This sets animation time to 0. This typically happens after an activity recreation, we don't
* want to re-animate the progress/success animation with the default timer
*/
private fun setAnimationTimeToZero() {
targetAnimationDuration = 0
}
/** This sets animation timers back to normal, this happens after we have */
private fun restoreAnimationTime() {
targetAnimationDuration = TARGET_ANIM_DURATION_LONG
}
companion object {
private const val TAG = "UdfpsEnrollDrawableV2"
private const val DEFAULT_STROKE_WIDTH = 3f
@@ -242,12 +268,13 @@ class UdfpsEnrollIconV2 internal constructor(context: Context, attrs: AttributeS
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
}
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
}
}

View File

@@ -25,28 +25,28 @@ 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 android.view.animation.OvershootInterpolator
import androidx.annotation.ColorInt
import androidx.core.animation.doOnEnd
import androidx.core.graphics.toRectF
import com.android.internal.annotations.VisibleForTesting
import com.android.settings.R
import kotlin.math.cos
import kotlin.math.max
import kotlin.math.sin
/**
* 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?) :
class UdfpsEnrollProgressBarDrawableV2(private val context: Context, attrs: AttributeSet?) :
Drawable() {
private val sensorRect: Rect = Rect()
private var rotation: Int = 0
private val strokeWidthPx: Float
@ColorInt private val progressColor: Int
@@ -56,7 +56,6 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
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
@@ -64,22 +63,27 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
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
private var checkMarkDrawable: Drawable
private var checkMarkAnimator: ValueAnimator? = null
private var fillColorAnimationDuration = FILL_COLOR_ANIMATION_DURATION_MS
private var animateArcDuration = PROGRESS_ANIMATION_DURATION_MS
private var checkmarkAnimationDelayDuration = CHECKMARK_ANIMATION_DELAY_MS
private var checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
init {
val ta =
mContext.obtainStyledAttributes(
context.obtainStyledAttributes(
attrs,
R.styleable.BiometricsEnrollView,
R.attr.biometricsEnrollStyle,
@@ -94,30 +98,33 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
enrollProgressHelpWithTalkback =
ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0)
ta.recycle()
val density = mContext.resources.displayMetrics.densityDpi.toFloat()
val density = context.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
}
backgroundPaint =
Paint().apply {
strokeWidth = strokeWidthPx
setColor(movingTargetFill)
isAntiAlias = true
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
checkMarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark)!!
// 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)!!
fillPaint =
Paint().apply {
strokeWidth = strokeWidthPx
setColor(progressColor)
isAntiAlias = true
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
progressBarRadius = mContext.resources.getInteger(R.integer.config_udfpsEnrollProgressBar)
progressBarRadius = context.resources.getInteger(R.integer.config_udfpsEnrollProgressBar)
progressUpdateListener = AnimatorUpdateListener { animation: ValueAnimator ->
progress = animation.getAnimatedValue() as Float
@@ -134,9 +141,10 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
}
/** Indicates enrollment progress has occurred. */
fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
fun onEnrollmentProgress(remaining: Int, totalSteps: Int, isRecreating: Boolean = false) {
afterFirstTouch = true
updateProgress(remaining, totalSteps)
updateProgress(remaining, totalSteps, isRecreating)
}
/** Indicates enrollment help has occurred. */
@@ -157,18 +165,12 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
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,
)
val sensorProgressRect = getSensorProgressRect()
// Rotate -90 degrees to make the progress start from the top right and not the bottom
// right
canvas.rotate(
-90f,
rotation - 90f,
sensorProgressRect.centerX().toFloat(),
sensorProgressRect.centerY().toFloat(),
)
@@ -176,9 +178,9 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
// Draw the background color of the progress circle.
canvas.drawArc(
sensorProgressRect.toRectF(),
0f /* startAngle */,
360f /* sweepAngle */,
false /* useCenter */,
0f, /* startAngle */
360f, /* sweepAngle */
false, /* useCenter */
backgroundPaint,
)
}
@@ -186,13 +188,15 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
// Draw the filled portion of the progress circle.
canvas.drawArc(
sensorProgressRect.toRectF(),
0f /* startAngle */,
360f * progress /* sweepAngle */,
false /* useCenter */,
0f, /* startAngle */
360f * progress, /* sweepAngle */
false, /* useCenter */
fillPaint,
)
}
canvas.restore()
checkMarkDrawable.draw(canvas)
}
/** Do nothing here, we will control the alpha internally. */
@@ -211,6 +215,7 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
*/
fun drawProgressAt(sensorRect: Rect) {
this.sensorRect.set(sensorRect)
invalidateSelf()
}
/** Indicates if accessibility is enabled or not. */
@@ -228,47 +233,21 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
}
}
private fun updateProgress(remainingSteps: Int, totalSteps: Int) {
private fun updateProgress(remainingSteps: Int, totalSteps: Int, isRecreating: Boolean) {
if (this.remainingSteps == remainingSteps && this.totalSteps == totalSteps) {
return
}
// 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()
}
this.remainingSteps = remainingSteps
this.totalSteps = totalSteps
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 targetProgress = (totalSteps - remainingSteps).toFloat().div(max(1, totalSteps))
@@ -276,12 +255,69 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
if (progressAnimator != null && progressAnimator!!.isRunning) {
progressAnimator!!.cancel()
}
/** The [progressUpdateListener] will force re-[draw]s to occur depending on the progress. */
progressAnimator =
ValueAnimator.ofFloat(progress, targetProgress).also {
it.setDuration(PROGRESS_ANIMATION_DURATION_MS)
it.setDuration(animateArcDuration)
it.addUpdateListener(progressUpdateListener)
it.start()
}
if (remainingSteps == 0) {
runCompletionAnimation()
}
}
private fun runCompletionAnimation() {
checkMarkAnimator?.cancel()
checkMarkAnimator = ValueAnimator.ofFloat(0f, 1f)
checkMarkAnimator?.apply {
startDelay = checkmarkAnimationDelayDuration
setDuration(checkmarkAnimationDuration)
interpolator = OvershootInterpolator()
addUpdateListener {
val newBounds = getCheckMarkStartBounds()
val scale = it.animatedFraction
newBounds.set(
newBounds.left,
newBounds.top,
(newBounds.left + (newBounds.width() * scale)).toInt(),
(newBounds.top + (newBounds.height() * scale)).toInt(),
)
checkMarkDrawable.bounds = newBounds
checkMarkDrawable.setVisible(true, false)
}
start()
}
}
/**
* This returns the bounds for which the checkmark drawable should be drawn at. It should be drawn
* on the arc of the progress bar at the 315 degree mark.
*/
private fun getCheckMarkStartBounds(): Rect {
val progressBounds = getSensorProgressRect()
val radius = progressBounds.width() / 2.0
var x = (cos(Math.toRadians(315.0)) * radius).toInt() + progressBounds.centerX()
// Remember to negate this value as sin(>180) will return negative value
var y = (-sin(Math.toRadians(315.0)) * radius).toInt() + progressBounds.centerY()
// Subtract height|width /2 to make sure we draw in the middle of the arc.
x -= (checkMarkDrawable.intrinsicWidth / 2.0).toInt()
y -= (checkMarkDrawable.intrinsicHeight / 2.0).toInt()
return Rect(x, y, x + checkMarkDrawable.intrinsicWidth, y + checkMarkDrawable.intrinsicHeight)
}
private fun getSensorProgressRect(): Rect {
val sensorProgressRect = Rect(sensorRect)
sensorProgressRect.inset(
-progressBarRadius,
-progressBarRadius,
-progressBarRadius,
-progressBarRadius,
)
return sensorProgressRect
}
/**
@@ -294,7 +330,7 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
}
backgroundColorAnimator =
ValueAnimator.ofArgb(backgroundPaint.color, onFirstBucketFailedColor).also {
it.setDuration(FILL_COLOR_ANIMATION_DURATION_MS)
it.setDuration(fillColorAnimationDuration)
it.repeatCount = 1
it.repeatMode = ValueAnimator.REVERSE
it.interpolator = DEACCEL
@@ -315,7 +351,7 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
@ColorInt val targetColor = helpColor
fillColorAnimator =
ValueAnimator.ofArgb(fillPaint.color, targetColor).also {
it.setDuration(FILL_COLOR_ANIMATION_DURATION_MS)
it.setDuration(fillColorAnimationDuration)
it.repeatCount = 1
it.repeatMode = ValueAnimator.REVERSE
it.interpolator = DEACCEL
@@ -325,33 +361,32 @@ class UdfpsEnrollProgressBarDrawableV2(private val mContext: Context, attrs: Att
}
}
private fun startCompletionAnimation() {
if (complete) {
return
}
complete = true
/**
* This sets animation time to 0. This typically happens after an activity recreation, we don't
* want to re-animate the progress/success animation with the default timer
*/
private fun setAnimationTimeToZero() {
fillColorAnimationDuration = 0
animateArcDuration = 0
checkmarkAnimationDelayDuration = 0
checkmarkAnimationDuration = 0
}
private fun rollBackCompletionAnimation() {
if (!complete) {
return
}
complete = false
/** This sets animation timers back to normal, this happens after we have */
private fun restoreAnimationTime() {
fillColorAnimationDuration = FILL_COLOR_ANIMATION_DURATION_MS
animateArcDuration = PROGRESS_ANIMATION_DURATION_MS
checkmarkAnimationDelayDuration = CHECKMARK_ANIMATION_DELAY_MS
checkmarkAnimationDuration = CHECKMARK_ANIMATION_DURATION_MS
}
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 CHECKMARK_ANIMATION_DELAY_MS = 200L
private const val CHECKMARK_ANIMATION_DURATION_MS = 300L
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)
}
}

View File

@@ -17,59 +17,99 @@
package com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.udfps.ui.widget
import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import android.util.AttributeSet
import android.util.Log
import android.view.DisplayInfo
import android.view.MotionEvent
import android.view.Surface
import android.view.View
import android.view.View.OnHoverListener
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
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
import com.android.systemui.biometrics.shared.model.toInt
/**
* 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 lateinit var fingerprintSensorType: FingerprintSensorType
private var onHoverListener: OnHoverListener = OnHoverListener { _, _ -> false }
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
private var remainingSteps = -1
private val udfpsUtils: UdfpsUtils = UdfpsUtils()
private lateinit var touchExplorationAnnouncer: TouchExplorationAnnouncer
private var isRecreating = false
/**
* 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].
* [FrameLayout] for the [UdfpsEnrollIconV2]. This function will also setup the
* [touchExplorationAnnouncer]
*/
fun setSensorRect(rect: Rect) {
fun setSensorRect(rect: Rect, sensorType: FingerprintSensorType) {
this.sensorRect = rect
this.fingerprintSensorType = sensorType
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 rotation = display.rotation
var displayInfo = DisplayInfo()
context.display.getDisplayInfo(displayInfo)
val scaleFactor = udfpsUtils.getScaleFactor(displayInfo)
val overlayParams =
UdfpsOverlayParams(
sensorRect,
fingerprintProgressDrawable.bounds,
displayInfo.naturalWidth,
displayInfo.naturalHeight,
scaleFactor,
rotation,
sensorType.toInt(),
)
val parentView = parent as ViewGroup
val coords = parentView.getLocationOnScreen()
val parentLeft = coords[0]
val parentTop = coords[1]
val sensorRectOffset = Rect(sensorRect)
// If the view has been rotated, we need to translate the sensor coordinates
// to the new rotated view.
when (rotation) {
Surface.ROTATION_90,
Surface.ROTATION_270 -> {
sensorRectOffset.set(
sensorRectOffset.top,
sensorRectOffset.left,
sensorRectOffset.bottom,
sensorRectOffset.right,
)
}
else -> {}
}
// Translate the sensor position into UdfpsEnrollView's view space.
sensorRectOffset.offset(-parentLeft, -parentTop)
fingerprintIcon.drawSensorRectAt(sensorRectOffset)
fingerprintProgressDrawable.drawProgressAt(sensorRectOffset)
touchExplorationAnnouncer = TouchExplorationAnnouncer(context, this, overlayParams, udfpsUtils)
}
/** Updates the current enrollment stage. */
@@ -78,15 +118,17 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
}
/** Receive enroll progress event */
fun onUdfpsEvent(event: UdfpsEnrollEvent) {
fun onUdfpsEvent(event: FingerEnrollState) {
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)
is FingerEnrollState.EnrollProgress ->
onEnrollmentProgress(event.remainingSteps, event.totalStepsRequired)
is FingerEnrollState.Acquired -> onAcquired(event.acquiredGood)
is FingerEnrollState.EnrollHelp -> onEnrollmentHelp()
is FingerEnrollState.PointerDown -> onPointerDown()
is FingerEnrollState.PointerUp -> onPointerUp()
is FingerEnrollState.OverlayShown -> overlayShown()
is FingerEnrollState.EnrollError ->
throw IllegalArgumentException("$TAG should not handle udfps error")
}
}
@@ -94,9 +136,37 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
fun setAccessibilityEnabled(enabled: Boolean) {
this.isAccessibilityEnabled = enabled
fingerprintProgressDrawable.setAccessibilityEnabled(enabled)
if (enabled) {
addHoverListener()
} else {
clearHoverListener()
}
}
private fun udfpsError(errMsgId: Int, errString: String) {}
/**
* Sends a touch exploration event to the [onHoverListener] this should only be used for
* debugging.
*/
fun sendDebugTouchExplorationEvent(motionEvent: MotionEvent) {
touchExplorationAnnouncer.onTouch(motionEvent)
}
/** Sets the addHoverListener, this should happen when talkback is enabled. */
private fun addHoverListener() {
onHoverListener = OnHoverListener { _: View, event: MotionEvent ->
sendDebugTouchExplorationEvent(event)
false
}
this.setOnHoverListener(onHoverListener)
}
/** Clears the hover listener if one was set. */
private fun clearHoverListener() {
val listener = OnHoverListener { _, _ -> false }
this.setOnHoverListener(listener)
onHoverListener = listener
}
private fun overlayShown() {
Log.e(TAG, "Implement overlayShown")
@@ -115,7 +185,7 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
/** Receive onAcquired event */
private fun onAcquired(isAcquiredGood: Boolean) {
val animateIfLastStepGood = isAcquiredGood && mRemainingSteps <= 2 && mRemainingSteps >= 0
val animateIfLastStepGood = isAcquiredGood && remainingSteps <= 2 && remainingSteps >= 0
if (animateIfLastStepGood) fingerprintProgressDrawable.onLastStepAcquired()
}
@@ -129,6 +199,52 @@ class UdfpsEnrollViewV2(context: Context, attrs: AttributeSet?) : FrameLayout(co
fingerprintIcon.startDrawing()
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
// Because the layout has changed, we need to recompute all locations.
if (this::sensorRect.isInitialized && this::fingerprintSensorType.isInitialized) {
setSensorRect(sensorRect, fingerprintSensorType)
}
}
/**
* This class is responsible for announcing touch events that are outside of the sensort rect
* area. Generally, if a touch is to the left of the sensor, the accessibility announcement will
* be something like "move right"
*/
private class TouchExplorationAnnouncer(
val context: Context,
val view: View,
val overlayParams: UdfpsOverlayParams,
val udfpsUtils: UdfpsUtils,
) {
/** Will announce accessibility event for touches outside of the sensor rect. */
fun onTouch(event: MotionEvent) {
val scaledTouch: Point =
udfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0), event, overlayParams)
if (udfpsUtils.isWithinSensorArea(event.getPointerId(0), event, overlayParams)) {
return
}
val theStr: String =
udfpsUtils.onTouchOutsideOfSensorArea(
true /*touchExplorationEnabled*/,
context,
scaledTouch.x,
scaledTouch.y,
overlayParams,
)
if (theStr != null) {
view.announceForAccessibility(theStr)
}
}
}
/** 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)
}
companion object {
private const val TAG = "UdfpsEnrollView"
}

View File

@@ -18,9 +18,11 @@ package com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.android.systemui.biometrics.shared.model.FingerprintSensor
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.update
@@ -63,7 +65,7 @@ class FingerprintEnrollEnrollingViewModel(
}
/** Collects the enrollment flow based on [enrollFlowShouldBeRunning] */
val enrollFLow =
val enrollFlow =
enrollFlowShouldBeRunning.transformLatest {
if (it) {
fingerprintEnrollViewModel.enrollFlow.collect { event -> emit(event) }

View File

@@ -25,7 +25,6 @@ import com.android.settings.biometrics.fingerprint2.domain.interactor.Orientatio
import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.lib.model.FingerEnrollState
import com.android.settings.biometrics.fingerprint2.lib.model.SetupWizard
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep.Education
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import kotlinx.coroutines.flow.Flow
@@ -38,7 +37,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** Models the UI state for [FingerprintEnrollFindSensorV2Fragment]. */
/** Models the UI state for fingerprint enroll education */
class FingerprintEnrollFindSensorViewModel(
private val navigationViewModel: FingerprintNavigationViewModel,
private val fingerprintEnrollViewModel: FingerprintEnrollViewModel,
@@ -70,7 +69,7 @@ class FingerprintEnrollFindSensorViewModel(
combineTransform(
_showSfpsLottie,
foldStateInteractor.isFolded,
orientationInteractor.rotation,
orientationInteractor.rotationFromDefault,
) { _, isFolded, rotation ->
emit(Pair(isFolded, rotation))
}
@@ -147,6 +146,7 @@ class FingerprintEnrollFindSensorViewModel(
}
}
is FingerEnrollState.EnrollHelp -> {}
else -> {}
}
}
}

View File

@@ -45,12 +45,14 @@ import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepositoryImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractorImpl
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintAuthAttemptModel
import com.android.settings.biometrics.fingerprint2.lib.model.FingerprintData
import com.android.settings.biometrics.fingerprint2.lib.model.Settings
import com.android.settings.biometrics.fingerprint2.ui.enrollment.modules.enrolling.common.util.toFingerprintEnrollOptions
import com.android.settings.biometrics.fingerprint2.ui.settings.binder.FingerprintSettingsViewBinder
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsNavigationViewModel
import com.android.settings.biometrics.fingerprint2.ui.settings.viewmodel.FingerprintSettingsViewModel
@@ -222,6 +224,13 @@ class FingerprintSettingsV2Fragment :
val fingerprintSensorProvider =
FingerprintSensorRepositoryImpl(fingerprintManager, backgroundDispatcher, lifecycleScope)
val pressToAuthInteractor = PressToAuthInteractorImpl(context, backgroundDispatcher)
val fingerprintEnrollStateRepository =
FingerprintEnrollInteractorImpl(
requireContext().applicationContext,
intent.toFingerprintEnrollOptions(),
fingerprintManager,
Settings,
)
val interactor =
FingerprintManagerInteractorImpl(
@@ -230,9 +239,7 @@ class FingerprintSettingsV2Fragment :
fingerprintManager,
fingerprintSensorProvider,
GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext)),
pressToAuthInteractor,
Settings,
getIntent()
fingerprintEnrollStateRepository,
)
val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)

View File

@@ -91,6 +91,7 @@ class Injector(step: FingerprintNavigationStep.UiStep) {
object : OrientationInteractor {
override val orientation: Flow<Int> = flowOf(Configuration.ORIENTATION_LANDSCAPE)
override val rotation: Flow<Int> = flowOf(Surface.ROTATION_0)
override val rotationFromDefault: Flow<Int> = rotation
override fun getRotationFromDefault(rotation: Int): Int = rotation
}

View File

@@ -17,9 +17,7 @@ package com.android.settings.tests.screenshot.biometrics.fingerprint.fragment
*/
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.FingerprintEnrollFindSensorV2Fragment
import com.android.settings.biometrics.fingerprint2.ui.enrollment.viewmodel.FingerprintNavigationStep
import com.android.settings.tests.screenshot.biometrics.fingerprint.Injector
import com.android.settings.biometrics.fingerprint2.ui.enrollment.fragment.education.RfpsEnrollFindSensorFragment
import com.android.settings.tests.screenshot.biometrics.fingerprint.Injector.Companion.BiometricFragmentScreenShotRule
import org.junit.Rule
import org.junit.Test
@@ -28,10 +26,7 @@ import platform.test.screenshot.FragmentScreenshotTestRule
import platform.test.screenshot.ViewScreenshotTestRule.Mode
@RunWith(AndroidJUnit4::class)
class FingerprintEnrollFindSensorScreenshotTest {
private val injector: Injector =
Injector(FingerprintNavigationStep.Education(Injector.interactor.sensorProp))
class RfpsEnrollFindSensorScreenshotTest {
@Rule @JvmField var rule: FragmentScreenshotTestRule = BiometricFragmentScreenShotRule()
@Test
@@ -39,7 +34,7 @@ class FingerprintEnrollFindSensorScreenshotTest {
rule.screenshotTest(
"fp_enroll_find_sensor",
Mode.MatchSize,
FingerprintEnrollFindSensorV2Fragment(injector.fingerprintSensor.sensorType, injector.factory),
RfpsEnrollFindSensorFragment(),
)
}
}

View File

@@ -33,8 +33,8 @@ import android.os.Handler
import androidx.test.core.app.ApplicationProvider
import com.android.settings.biometrics.GatekeeperPasswordProvider
import com.android.settings.biometrics.fingerprint2.data.repository.FingerprintSensorRepository
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintEnrollInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
import com.android.settings.biometrics.fingerprint2.domain.interactor.PressToAuthInteractor
import com.android.settings.biometrics.fingerprint2.lib.domain.interactor.FingerprintManagerInteractor
import com.android.settings.biometrics.fingerprint2.lib.model.Default
import com.android.settings.biometrics.fingerprint2.lib.model.EnrollReason
@@ -82,10 +82,6 @@ class FingerprintManagerInteractorTest {
@Mock private lateinit var gateKeeperPasswordProvider: GatekeeperPasswordProvider
private var testScope = TestScope(backgroundDispatcher)
private var pressToAuthInteractor =
object : PressToAuthInteractor {
override val isEnabled = flowOf(false)
}
@Before
fun setup() {
@@ -113,9 +109,12 @@ class FingerprintManagerInteractorTest {
fingerprintManager,
fingerprintSensorRepository,
gateKeeperPasswordProvider,
pressToAuthInteractor,
Default,
Intent(),
FingerprintEnrollInteractorImpl(
context,
FingerprintEnrollOptions.Builder().build(),
fingerprintManager,
Default,
),
)
}

View File

@@ -145,7 +145,8 @@ class FingerprintEnrollFindSensorViewModelV2Test {
orientationInteractor =
object : OrientationInteractor {
override val orientation: Flow<Int> = flowOf(Configuration.ORIENTATION_LANDSCAPE)
override val rotation: Flow<Int> = flowOf(Surface.ROTATION_0)
override val rotation: Flow<Int> = flowOf(Surface.ROTATION_0)
override val rotationFromDefault: Flow<Int> = flowOf(Surface.ROTATION_0)
override fun getRotationFromDefault(rotation: Int): Int = rotation
}
underTest =