Merge "Revert "Revert "Revert "FingerprintSensorProperties refactor - ui layer"""" into udc-qpr-dev am: 213e0ec1b8

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/24150917

Change-Id: I7e7919807ff57dcf4001ed5769e88b742e976cf8
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Beverly Tai
2023-07-22 01:18:06 +00:00
committed by Automerger Merge Worker
10 changed files with 422 additions and 715 deletions

View File

@@ -17,7 +17,12 @@ package com.android.systemui.biometrics
import android.app.ActivityTaskManager import android.app.ActivityTaskManager
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants import android.hardware.biometrics.BiometricOverlayConstants
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -28,23 +33,27 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController import android.hardware.fingerprint.ISidefpsController
import android.os.Handler import android.os.Handler
import android.util.Log import android.util.Log
import android.util.RotationUtils
import android.view.Display import android.view.Display
import android.view.DisplayInfo import android.view.DisplayInfo
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Surface import android.view.Surface
import android.view.View import android.view.View
import android.view.View.AccessibilityDelegate
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
import android.view.WindowManager import android.view.WindowManager
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
import android.view.accessibility.AccessibilityEvent
import androidx.annotation.RawRes
import com.airbnb.lottie.LottieAnimationView import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import com.android.internal.annotations.VisibleForTesting import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
@@ -55,7 +64,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.traceSection import com.android.systemui.util.traceSection
import java.io.PrintWriter import java.io.PrintWriter
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -78,7 +86,6 @@ constructor(
@Main private val mainExecutor: DelayableExecutor, @Main private val mainExecutor: DelayableExecutor,
@Main private val handler: Handler, @Main private val handler: Handler,
private val alternateBouncerInteractor: AlternateBouncerInteractor, private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
@Application private val scope: CoroutineScope, @Application private val scope: CoroutineScope,
dumpManager: DumpManager dumpManager: DumpManager
) : Dumpable { ) : Dumpable {
@@ -243,15 +250,105 @@ constructor(
private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) { private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false) val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
overlayView = view overlayView = view
SideFpsOverlayViewBinder.bind( val display = context.display!!
view = view, // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
viewModel = sideFpsOverlayViewModelFactory.get(), display.getDisplayInfo(displayInfo)
overlayViewParams = overlayViewParams, val offsets =
reason = reason, sensorProps.getLocation(display.uniqueId).let { location ->
context = context, if (location == null) {
Log.w(TAG, "No location specified for display: ${display.uniqueId}")
}
location ?: sensorProps.location
}
overlayOffsets = offsets
val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
view.rotation =
display.asSideFpsAnimationRotation(
offsets.isYAligned(),
getRotationFromDefault(displayInfo.rotation)
)
lottie.setAnimation(
display.asSideFpsAnimation(
offsets.isYAligned(),
getRotationFromDefault(displayInfo.rotation)
)
) )
lottie.addLottieOnCompositionLoadedListener {
// Check that view is not stale, and that overlayView has not been hidden/removed
if (overlayView != null && overlayView == view) {
updateOverlayParams(display, it.bounds)
}
}
orientationReasonListener.reason = reason orientationReasonListener.reason = reason
lottie.addOverlayDynamicColor(context, reason)
/**
* Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
* speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
* in focus
*/
view.setAccessibilityDelegate(
object : AccessibilityDelegate() {
override fun dispatchPopulateAccessibilityEvent(
host: View,
event: AccessibilityEvent
): Boolean {
return if (
event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
) {
true
} else {
super.dispatchPopulateAccessibilityEvent(host, event)
}
}
}
)
} }
@VisibleForTesting
fun updateOverlayParams(display: Display, bounds: Rect) {
val isNaturalOrientation = display.isNaturalOrientation()
val isDefaultOrientation =
if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
val size = windowManager.maximumWindowMetrics.bounds
val displayWidth = if (isDefaultOrientation) size.width() else size.height()
val displayHeight = if (isDefaultOrientation) size.height() else size.width()
val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
val sensorBounds =
if (overlayOffsets.isYAligned()) {
Rect(
displayWidth - boundsWidth,
overlayOffsets.sensorLocationY,
displayWidth,
overlayOffsets.sensorLocationY + boundsHeight
)
} else {
Rect(
overlayOffsets.sensorLocationX,
0,
overlayOffsets.sensorLocationX + boundsWidth,
boundsHeight
)
}
RotationUtils.rotateBounds(
sensorBounds,
Rect(0, 0, displayWidth, displayHeight),
getRotationFromDefault(display.rotation)
)
overlayViewParams.x = sensorBounds.left
overlayViewParams.y = sensorBounds.top
windowManager.updateViewLayout(overlayView, overlayViewParams)
}
private fun getRotationFromDefault(rotation: Int): Int =
if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
} }
private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal? private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
@@ -276,12 +373,89 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo
private fun ActivityTaskManager.topClass(): String = private fun ActivityTaskManager.topClass(): String =
getTasks(1).firstOrNull()?.topActivity?.className ?: "" getTasks(1).firstOrNull()?.topActivity?.className ?: ""
@RawRes
private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
when (rotationFromDefault) {
Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
}
private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
when (rotationFromDefault) {
Surface.ROTATION_90 -> if (yAligned) 0f else 180f
Surface.ROTATION_180 -> 180f
Surface.ROTATION_270 -> if (yAligned) 180f else 0f
else -> 0f
}
private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0 private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
private fun Display.isNaturalOrientation(): Boolean = private fun Display.isNaturalOrientation(): Boolean =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
public class OrientationReasonListener( private fun LottieAnimationView.addOverlayDynamicColor(
context: Context,
@BiometricOverlayConstants.ShowReason reason: Int
) {
fun update() {
val isKeyguard = reason == REASON_AUTH_KEYGUARD
if (isKeyguard) {
val color =
com.android.settingslib.Utils.getColorAttrDefaultColor(
context,
com.android.internal.R.attr.materialColorPrimaryFixed
)
val outerRimColor =
com.android.settingslib.Utils.getColorAttrDefaultColor(
context,
com.android.internal.R.attr.materialColorPrimaryFixedDim
)
val chevronFill =
com.android.settingslib.Utils.getColorAttrDefaultColor(
context,
com.android.internal.R.attr.materialColorOnPrimaryFixed
)
addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
}
addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
}
addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
}
} else {
if (!isDarkMode(context)) {
addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
}
}
for (key in listOf(".blue600", ".blue400")) {
addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(
context.getColor(R.color.settingslib_color_blue400),
PorterDuff.Mode.SRC_ATOP
)
}
}
}
}
if (composition != null) {
update()
} else {
addLottieOnCompositionLoadedListener { update() }
}
}
private fun isDarkMode(context: Context): Boolean {
val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return darkMode == Configuration.UI_MODE_NIGHT_YES
}
@VisibleForTesting
class OrientationReasonListener(
context: Context, context: Context,
displayManager: DisplayManager, displayManager: DisplayManager,
handler: Handler, handler: Handler,

View File

@@ -16,11 +16,8 @@
package com.android.systemui.biometrics.data.repository package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.SensorLocationInternal import android.hardware.biometrics.SensorLocationInternal
import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintManager import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -33,8 +30,10 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
/** /**
@@ -44,17 +43,22 @@ import kotlinx.coroutines.flow.shareIn
*/ */
interface FingerprintPropertyRepository { interface FingerprintPropertyRepository {
/**
* If the repository is initialized or not. Other properties are defaults until this is true.
*/
val isInitialized: Flow<Boolean>
/** The id of fingerprint sensor. */ /** The id of fingerprint sensor. */
val sensorId: Flow<Int> val sensorId: StateFlow<Int>
/** The security strength of sensor (convenience, weak, strong). */ /** The security strength of sensor (convenience, weak, strong). */
val strength: Flow<SensorStrength> val strength: StateFlow<SensorStrength>
/** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */ /** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
val sensorType: Flow<FingerprintSensorType> val sensorType: StateFlow<FingerprintSensorType>
/** The sensor location relative to each physical display. */ /** The sensor location relative to each physical display. */
val sensorLocations: Flow<Map<String, SensorLocationInternal>> val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
} }
@SysUISingleton @SysUISingleton
@@ -62,10 +66,10 @@ class FingerprintPropertyRepositoryImpl
@Inject @Inject
constructor( constructor(
@Application private val applicationScope: CoroutineScope, @Application private val applicationScope: CoroutineScope,
private val fingerprintManager: FingerprintManager? private val fingerprintManager: FingerprintManager?,
) : FingerprintPropertyRepository { ) : FingerprintPropertyRepository {
private val props: Flow<FingerprintSensorPropertiesInternal> = override val isInitialized: Flow<Boolean> =
conflatedCallbackFlow { conflatedCallbackFlow {
val callback = val callback =
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() { object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -73,47 +77,45 @@ constructor(
sensors: List<FingerprintSensorPropertiesInternal> sensors: List<FingerprintSensorPropertiesInternal>
) { ) {
if (sensors.isNotEmpty()) { if (sensors.isNotEmpty()) {
trySendWithFailureLogging(sensors[0], TAG, "initialize properties") setProperties(sensors[0])
} else { trySendWithFailureLogging(true, TAG, "initialize properties")
trySendWithFailureLogging(
DEFAULT_PROPS,
TAG,
"initialize with default properties"
)
} }
} }
} }
fingerprintManager?.addAuthenticatorsRegisteredCallback(callback) fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties") trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
awaitClose {} awaitClose {}
} }
.shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1) .shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
override val sensorId: Flow<Int> = props.map { it.sensorId } private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
override val strength: Flow<SensorStrength> = override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
props.map { sensorStrengthIntToObject(it.sensorStrength) }
override val sensorType: Flow<FingerprintSensorType> = private val _strength: MutableStateFlow<SensorStrength> =
props.map { sensorTypeIntToObject(it.sensorType) } MutableStateFlow(SensorStrength.CONVENIENCE)
override val sensorLocations: Flow<Map<String, SensorLocationInternal>> = override val strength = _strength.asStateFlow()
props.map {
it.allLocations.associateBy { sensorLocationInternal -> private val _sensorType: MutableStateFlow<FingerprintSensorType> =
MutableStateFlow(FingerprintSensorType.UNKNOWN)
override val sensorType = _sensorType.asStateFlow()
private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
_sensorLocations.asStateFlow()
private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
_sensorId.value = prop.sensorId
_strength.value = sensorStrengthIntToObject(prop.sensorStrength)
_sensorType.value = sensorTypeIntToObject(prop.sensorType)
_sensorLocations.value =
prop.allLocations.associateBy { sensorLocationInternal ->
sensorLocationInternal.displayId sensorLocationInternal.displayId
} }
} }
companion object { companion object {
private const val TAG = "FingerprintPropertyRepositoryImpl" private const val TAG = "FingerprintPropertyRepositoryImpl"
private val DEFAULT_PROPS =
FingerprintSensorPropertiesInternal(
-1 /* sensorId */,
SensorProperties.STRENGTH_CONVENIENCE,
0 /* maxEnrollmentsPerUser */,
listOf<ComponentInfoInternal>(),
FingerprintSensorProperties.TYPE_UNKNOWN,
false /* halControlsIllumination */,
true /* resetLockoutRequiresHardwareAuthToken */,
listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
)
} }
} }

View File

@@ -17,24 +17,16 @@
package com.android.systemui.biometrics.domain.interactor package com.android.systemui.biometrics.domain.interactor
import android.hardware.biometrics.SensorLocationInternal import android.hardware.biometrics.SensorLocationInternal
import android.util.Log
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
/** Business logic for SideFps overlay offsets. */ /** Business logic for SideFps overlay offsets. */
interface SideFpsOverlayInteractor { interface SideFpsOverlayInteractor {
/** The displayId of the current display. */
val displayId: Flow<String>
/** The corresponding offsets based on different displayId. */ /** Get the corresponding offsets based on different displayId. */
val overlayOffsets: Flow<SensorLocationInternal> fun getOverlayOffsets(displayId: String): SensorLocationInternal
/** Update the displayId. */
fun changeDisplay(displayId: String?)
} }
@SysUISingleton @SysUISingleton
@@ -43,16 +35,14 @@ class SideFpsOverlayInteractorImpl
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) : constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
SideFpsOverlayInteractor { SideFpsOverlayInteractor {
private val _displayId: MutableStateFlow<String> = MutableStateFlow("") override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
override val displayId: Flow<String> = _displayId.asStateFlow() val offsets = fingerprintPropertyRepository.sensorLocations.value
return if (offsets.containsKey(displayId)) {
override val overlayOffsets: Flow<SensorLocationInternal> = offsets[displayId]!!
combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets -> } else {
offsets[displayId] ?: SensorLocationInternal.DEFAULT Log.w(TAG, "No location specified for display: $displayId")
offsets[""]!!
} }
override fun changeDisplay(displayId: String?) {
_displayId.value = displayId ?: ""
} }
companion object { companion object {

View File

@@ -1,168 +0,0 @@
/*
* Copyright (C) 2023 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.systemui.biometrics.ui.binder
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.hardware.biometrics.BiometricOverlayConstants
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
import com.android.systemui.R
import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.lifecycle.repeatWhenAttached
import kotlinx.coroutines.launch
/** Sub-binder for SideFpsOverlayView. */
object SideFpsOverlayViewBinder {
/** Bind the view. */
@JvmStatic
fun bind(
view: View,
viewModel: SideFpsOverlayViewModel,
overlayViewParams: WindowManager.LayoutParams,
@BiometricOverlayConstants.ShowReason reason: Int,
@Application context: Context
) {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
viewModel.changeDisplay()
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.sideFpsAnimationRotation.collect { rotation ->
view.rotation = rotation
}
}
launch {
// TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation
// in order to add scuba tests in the future.
viewModel.sideFpsAnimation.collect { animation ->
lottie.setAnimation(animation)
}
}
launch {
viewModel.sensorBounds.collect { sensorBounds ->
overlayViewParams.x = sensorBounds.left
overlayViewParams.y = sensorBounds.top
windowManager.updateViewLayout(view, overlayViewParams)
}
}
launch {
viewModel.overlayOffsets.collect { overlayOffsets ->
lottie.addLottieOnCompositionLoadedListener {
viewModel.updateSensorBounds(
it.bounds,
windowManager.maximumWindowMetrics.bounds,
overlayOffsets
)
}
}
}
}
}
lottie.addOverlayDynamicColor(context, reason)
/**
* Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
* speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
* in focus
*/
view.accessibilityDelegate =
object : View.AccessibilityDelegate() {
override fun dispatchPopulateAccessibilityEvent(
host: View,
event: AccessibilityEvent
): Boolean {
return if (
event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
) {
true
} else {
super.dispatchPopulateAccessibilityEvent(host, event)
}
}
}
}
}
private fun LottieAnimationView.addOverlayDynamicColor(
context: Context,
@BiometricOverlayConstants.ShowReason reason: Int
) {
fun update() {
val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD
if (isKeyguard) {
val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color
val chevronFill =
com.android.settingslib.Utils.getColorAttrDefaultColor(
context,
android.R.attr.textColorPrimaryInverse
)
for (key in listOf(".blue600", ".blue400")) {
addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
}
}
addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
}
} else if (!isDarkMode(context)) {
addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
}
} else if (isDarkMode(context)) {
for (key in listOf(".blue600", ".blue400")) {
addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
PorterDuffColorFilter(
context.getColor(R.color.settingslib_color_blue400),
PorterDuff.Mode.SRC_ATOP
)
}
}
}
}
if (composition != null) {
update()
} else {
addLottieOnCompositionLoadedListener { update() }
}
}
private fun isDarkMode(context: Context): Boolean {
val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return darkMode == Configuration.UI_MODE_NIGHT_YES
}

View File

@@ -1,148 +0,0 @@
/*
* Copyright (C) 2023 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.systemui.biometrics.ui.viewmodel
import android.content.Context
import android.graphics.Rect
import android.hardware.biometrics.SensorLocationInternal
import android.util.RotationUtils
import android.view.Display
import android.view.DisplayInfo
import android.view.Surface
import androidx.annotation.RawRes
import com.android.systemui.R
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
import com.android.systemui.dagger.qualifiers.Application
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
/** View-model for SideFpsOverlayView. */
class SideFpsOverlayViewModel
@Inject
constructor(
@Application private val context: Context,
private val sideFpsOverlayInteractor: SideFpsOverlayInteractor,
) {
private val isReverseDefaultRotation =
context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
private val _sensorBounds: MutableStateFlow<Rect> = MutableStateFlow(Rect())
val sensorBounds = _sensorBounds.asStateFlow()
val overlayOffsets: Flow<SensorLocationInternal> = sideFpsOverlayInteractor.overlayOffsets
/** Update the displayId. */
fun changeDisplay() {
sideFpsOverlayInteractor.changeDisplay(context.display!!.uniqueId)
}
/** Determine the rotation of the sideFps animation given the overlay offsets. */
val sideFpsAnimationRotation: Flow<Float> =
overlayOffsets.map { overlayOffsets ->
val display = context.display!!
val displayInfo: DisplayInfo = DisplayInfo()
// b/284098873 `context.display.rotation` may not up-to-date, we use
// displayInfo.rotation
display.getDisplayInfo(displayInfo)
val yAligned: Boolean = overlayOffsets.isYAligned()
when (getRotationFromDefault(displayInfo.rotation)) {
Surface.ROTATION_90 -> if (yAligned) 0f else 180f
Surface.ROTATION_180 -> 180f
Surface.ROTATION_270 -> if (yAligned) 180f else 0f
else -> 0f
}
}
/** Populate the sideFps animation from the overlay offsets. */
@RawRes
val sideFpsAnimation: Flow<Int> =
overlayOffsets.map { overlayOffsets ->
val display = context.display!!
val displayInfo: DisplayInfo = DisplayInfo()
// b/284098873 `context.display.rotation` may not up-to-date, we use
// displayInfo.rotation
display.getDisplayInfo(displayInfo)
val yAligned: Boolean = overlayOffsets.isYAligned()
when (getRotationFromDefault(displayInfo.rotation)) {
Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
Surface.ROTATION_180 ->
if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
}
}
/**
* Calculate and update the bounds of the sensor based on the bounds of the overlay view, the
* maximum bounds of the window, and the offsets of the sensor location.
*/
fun updateSensorBounds(
bounds: Rect,
maximumWindowBounds: Rect,
offsets: SensorLocationInternal
) {
val isNaturalOrientation = context.display!!.isNaturalOrientation()
val isDefaultOrientation =
if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
val displayWidth =
if (isDefaultOrientation) maximumWindowBounds.width() else maximumWindowBounds.height()
val displayHeight =
if (isDefaultOrientation) maximumWindowBounds.height() else maximumWindowBounds.width()
val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
val sensorBounds =
if (offsets.isYAligned()) {
Rect(
displayWidth - boundsWidth,
offsets.sensorLocationY,
displayWidth,
offsets.sensorLocationY + boundsHeight
)
} else {
Rect(
offsets.sensorLocationX,
0,
offsets.sensorLocationX + boundsWidth,
boundsHeight
)
}
val displayInfo: DisplayInfo = DisplayInfo()
context.display!!.getDisplayInfo(displayInfo)
RotationUtils.rotateBounds(
sensorBounds,
Rect(0, 0, displayWidth, displayHeight),
getRotationFromDefault(displayInfo.rotation)
)
_sensorBounds.value = sensorBounds
}
private fun getRotationFromDefault(rotation: Int): Int =
if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
}
private fun Display.isNaturalOrientation(): Boolean =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0

View File

@@ -44,6 +44,9 @@ import android.view.View
import android.view.ViewPropertyAnimator import android.view.ViewPropertyAnimator
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowManager import android.view.WindowManager
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
import android.view.WindowMetrics import android.view.WindowMetrics
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
@@ -53,14 +56,9 @@ import com.android.systemui.R
import com.android.systemui.RoboPilotTest import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext import com.android.systemui.SysuiTestableContext
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
@@ -101,7 +99,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
@SmallTest @SmallTest
@RoboPilotTest @RoboPilotTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true) @TestableLooper.RunWithLooper
class SideFpsControllerTest : SysuiTestCase() { class SideFpsControllerTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule() @JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@ class SideFpsControllerTest : SysuiTestCase() {
private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
private lateinit var displayStateInteractor: DisplayStateInteractor private lateinit var displayStateInteractor: DisplayStateInteractor
private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel
private val fingerprintRepository = FakeFingerprintPropertyRepository()
private val executor = FakeExecutor(FakeSystemClock()) private val executor = FakeExecutor(FakeSystemClock())
private val rearDisplayStateRepository = FakeRearDisplayStateRepository() private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
@@ -163,15 +159,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor, executor,
rearDisplayStateRepository rearDisplayStateRepository
) )
sideFpsOverlayViewModel =
SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository))
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.REAR,
sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0))
)
context.addMockSystemService(DisplayManager::class.java, displayManager) context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, windowManager) context.addMockSystemService(WindowManager::class.java, windowManager)
@@ -278,7 +265,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor, executor,
handler, handler,
alternateBouncerInteractor, alternateBouncerInteractor,
{ sideFpsOverlayViewModel },
TestCoroutineScope(), TestCoroutineScope(),
dumpManager dumpManager
) )
@@ -697,6 +683,106 @@ class SideFpsControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
} }
/**
* {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
* and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
* rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
* in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForXAlignedSensor_0() =
testWithDisplay(
deviceConfig = DeviceConfig.X_ALIGNED,
isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_0 }
) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
}
/**
* {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
* in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
* indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
* correctly, tests for indicator placement in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
testWithDisplay(
deviceConfig = DeviceConfig.X_ALIGNED,
isReverseDefaultRotation = true,
{ rotation = Surface.ROTATION_270 }
) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
}
/**
* {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
* and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
* rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
* in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForYAlignedSensor_0() =
testWithDisplay(
deviceConfig = DeviceConfig.Y_ALIGNED,
isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_0 }
) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
}
/**
* {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
* in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
* indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
* correctly, tests for indicator placement in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
testWithDisplay(
deviceConfig = DeviceConfig.Y_ALIGNED,
isReverseDefaultRotation = true,
{ rotation = Surface.ROTATION_270 }
) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
}
@Test @Test
fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay { fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
// By default all those tests assume the side fps sensor is available. // By default all those tests assume the side fps sensor is available.
@@ -709,6 +795,51 @@ class SideFpsControllerTest : SysuiTestCase() {
assertThat(fingerprintManager.hasSideFpsSensor()).isFalse() assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
} }
@Test
fun testLayoutParams_isKeyguardDialogType() =
testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
val lpType = overlayViewParamsCaptor.value.type
assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue()
}
@Test
fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
val lpFlags = overlayViewParamsCaptor.value.privateFlags
assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
}
@Test
fun testLayoutParams_hasTrustedOverlayWindowFlag() =
testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
sideFpsController.overlayOffsets = sensorLocation
sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
val lpFlags = overlayViewParamsCaptor.value.privateFlags
assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
}
} }
private fun insetsForSmallNavbar() = insetsWithBottom(60) private fun insetsForSmallNavbar() = insetsWithBottom(60)

View File

@@ -73,15 +73,10 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
@Test @Test
fun initializeProperties() = fun initializeProperties() =
testScope.runTest { testScope.runTest {
val sensorId by collectLastValue(repository.sensorId) val isInitialized = collectLastValue(repository.isInitialized)
val strength by collectLastValue(repository.strength)
val sensorType by collectLastValue(repository.sensorType)
val sensorLocations by collectLastValue(repository.sensorLocations)
// Assert default properties. assertDefaultProperties()
assertThat(sensorId).isEqualTo(-1) assertThat(isInitialized()).isFalse()
assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
val fingerprintProps = val fingerprintProps =
listOf( listOf(
@@ -120,24 +115,31 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps) fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
assertThat(sensorId).isEqualTo(1) assertThat(repository.sensorId.value).isEqualTo(1)
assertThat(strength).isEqualTo(SensorStrength.STRONG) assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR) assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
assertThat(sensorLocations?.size).isEqualTo(2) assertThat(repository.sensorLocations.value.size).isEqualTo(2)
assertThat(sensorLocations).containsKey("display_id_1") assertThat(repository.sensorLocations.value).containsKey("display_id_1")
with(sensorLocations?.get("display_id_1")!!) { with(repository.sensorLocations.value["display_id_1"]!!) {
assertThat(displayId).isEqualTo("display_id_1") assertThat(displayId).isEqualTo("display_id_1")
assertThat(sensorLocationX).isEqualTo(100) assertThat(sensorLocationX).isEqualTo(100)
assertThat(sensorLocationY).isEqualTo(300) assertThat(sensorLocationY).isEqualTo(300)
assertThat(sensorRadius).isEqualTo(20) assertThat(sensorRadius).isEqualTo(20)
} }
assertThat(sensorLocations).containsKey("") assertThat(repository.sensorLocations.value).containsKey("")
with(sensorLocations?.get("")!!) { with(repository.sensorLocations.value[""]!!) {
assertThat(displayId).isEqualTo("") assertThat(displayId).isEqualTo("")
assertThat(sensorLocationX).isEqualTo(540) assertThat(sensorLocationX).isEqualTo(540)
assertThat(sensorLocationY).isEqualTo(1636) assertThat(sensorLocationY).isEqualTo(1636)
assertThat(sensorRadius).isEqualTo(130) assertThat(sensorRadius).isEqualTo(130)
} }
assertThat(isInitialized()).isTrue()
} }
private fun assertDefaultProperties() {
assertThat(repository.sensorId.value).isEqualTo(-1)
assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE)
assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN)
}
} }

View File

@@ -22,7 +22,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@@ -52,9 +51,8 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
} }
@Test @Test
fun testGetOverlayoffsets() = fun testGetOverlayOffsets() =
testScope.runTest { testScope.runTest {
// Arrange.
fingerprintRepository.setProperties( fingerprintRepository.setProperties(
sensorId = 1, sensorId = 1,
strength = SensorStrength.STRONG, strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
) )
) )
// Act. var offsets = interactor.getOverlayOffsets("display_id_1")
val offsets by collectLastValue(interactor.overlayOffsets) assertThat(offsets.displayId).isEqualTo("display_id_1")
val displayId by collectLastValue(interactor.displayId) assertThat(offsets.sensorLocationX).isEqualTo(100)
assertThat(offsets.sensorLocationY).isEqualTo(300)
assertThat(offsets.sensorRadius).isEqualTo(20)
// Assert offsets of empty displayId. offsets = interactor.getOverlayOffsets("invalid_display_id")
assertThat(displayId).isEqualTo("") assertThat(offsets.displayId).isEqualTo("")
assertThat(offsets?.displayId).isEqualTo("") assertThat(offsets.sensorLocationX).isEqualTo(540)
assertThat(offsets?.sensorLocationX).isEqualTo(540) assertThat(offsets.sensorLocationY).isEqualTo(1636)
assertThat(offsets?.sensorLocationY).isEqualTo(1636) assertThat(offsets.sensorRadius).isEqualTo(130)
assertThat(offsets?.sensorRadius).isEqualTo(130)
// Offsets should be updated correctly.
interactor.changeDisplay("display_id_1")
assertThat(displayId).isEqualTo("display_id_1")
assertThat(offsets?.displayId).isEqualTo("display_id_1")
assertThat(offsets?.sensorLocationX).isEqualTo(100)
assertThat(offsets?.sensorLocationY).isEqualTo(300)
assertThat(offsets?.sensorRadius).isEqualTo(20)
// Should return default offset when the displayId is invalid.
interactor.changeDisplay("invalid_display_id")
assertThat(displayId).isEqualTo("invalid_display_id")
assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId)
assertThat(offsets?.sensorLocationX)
.isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX)
assertThat(offsets?.sensorLocationY)
.isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY)
assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius)
} }
} }

View File

@@ -1,263 +0,0 @@
/*
* Copyright (C) 2023 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.systemui.biometrics.ui.viewmodel
import android.graphics.Rect
import android.hardware.biometrics.SensorLocationInternal
import android.hardware.display.DisplayManagerGlobal
import android.view.Display
import android.view.DisplayAdjustments
import android.view.DisplayInfo
import android.view.Surface
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor
import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.junit.MockitoJUnit
private const val DISPLAY_ID = 2
@SmallTest
@RunWith(JUnit4::class)
class SideFpsOverlayViewModelTest : SysuiTestCase() {
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
private var testScope: TestScope = TestScope(StandardTestDispatcher())
private val fingerprintRepository = FakeFingerprintPropertyRepository()
private lateinit var interactor: SideFpsOverlayInteractor
private lateinit var viewModel: SideFpsOverlayViewModel
enum class DeviceConfig {
X_ALIGNED,
Y_ALIGNED,
}
private lateinit var deviceConfig: DeviceConfig
private lateinit var indicatorBounds: Rect
private lateinit var displayBounds: Rect
private lateinit var sensorLocation: SensorLocationInternal
private var displayWidth: Int = 0
private var displayHeight: Int = 0
private var boundsWidth: Int = 0
private var boundsHeight: Int = 0
@Before
fun setup() {
interactor = SideFpsOverlayInteractorImpl(fingerprintRepository)
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
sensorType = FingerprintSensorType.REAR,
sensorLocations =
mapOf(
"" to
SensorLocationInternal(
"" /* displayId */,
540 /* sensorLocationX */,
1636 /* sensorLocationY */,
130 /* sensorRadius */
),
"display_id_1" to
SensorLocationInternal(
"display_id_1" /* displayId */,
100 /* sensorLocationX */,
300 /* sensorLocationY */,
20 /* sensorRadius */
)
)
)
}
@Test
fun testOverlayOffsets() =
testScope.runTest {
viewModel = SideFpsOverlayViewModel(mContext, interactor)
val interactorOffsets by collectLastValue(interactor.overlayOffsets)
val viewModelOffsets by collectLastValue(viewModel.overlayOffsets)
assertThat(viewModelOffsets).isEqualTo(interactorOffsets)
}
private fun testWithDisplay(
deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
isReverseDefaultRotation: Boolean = false,
initInfo: DisplayInfo.() -> Unit = {},
block: () -> Unit
) {
this.deviceConfig = deviceConfig
when (deviceConfig) {
DeviceConfig.X_ALIGNED -> {
displayWidth = 3000
displayHeight = 1500
sensorLocation = SensorLocationInternal("", 2500, 0, 0)
boundsWidth = 200
boundsHeight = 100
}
DeviceConfig.Y_ALIGNED -> {
displayWidth = 2500
displayHeight = 2000
sensorLocation = SensorLocationInternal("", 0, 300, 0)
boundsWidth = 100
boundsHeight = 200
}
}
indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
displayBounds = Rect(0, 0, displayWidth, displayHeight)
val displayInfo = DisplayInfo()
displayInfo.initInfo()
val dmGlobal = Mockito.mock(DisplayManagerGlobal::class.java)
val display =
Display(
dmGlobal,
DISPLAY_ID,
displayInfo,
DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
)
whenever(dmGlobal.getDisplayInfo(ArgumentMatchers.eq(DISPLAY_ID))).thenReturn(displayInfo)
val sideFpsOverlayViewModelContext =
context.createDisplayContext(display) as SysuiTestableContext
sideFpsOverlayViewModelContext.orCreateTestableResources.addOverride(
com.android.internal.R.bool.config_reverseDefaultRotation,
isReverseDefaultRotation
)
viewModel = SideFpsOverlayViewModel(sideFpsOverlayViewModelContext, interactor)
block()
}
/**
* {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
* ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
* the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
* placement in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForXAlignedSensor_0() =
testScope.runTest {
testWithDisplay(
deviceConfig = DeviceConfig.X_ALIGNED,
isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_0 }
) {
viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
val displayInfo: DisplayInfo = DisplayInfo()
context.display!!.getDisplayInfo(displayInfo)
assertThat(displayInfo.rotation).isEqualTo(Surface.ROTATION_0)
assertThat(viewModel.sensorBounds.value).isNotNull()
assertThat(viewModel.sensorBounds.value.left)
.isEqualTo(sensorLocation.sensorLocationX)
assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
}
}
/**
* {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
* ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
* correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
* works correctly, tests for indicator placement in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
testScope.runTest {
testWithDisplay(
deviceConfig = DeviceConfig.X_ALIGNED,
isReverseDefaultRotation = true,
{ rotation = Surface.ROTATION_270 }
) {
viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
assertThat(viewModel.sensorBounds.value).isNotNull()
assertThat(viewModel.sensorBounds.value.left)
.isEqualTo(sensorLocation.sensorLocationX)
assertThat(viewModel.sensorBounds.value.top).isEqualTo(0)
}
}
/**
* {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
* ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given
* the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator
* placement in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForYAlignedSensor_0() =
testScope.runTest {
testWithDisplay(
deviceConfig = DeviceConfig.Y_ALIGNED,
isReverseDefaultRotation = false,
{ rotation = Surface.ROTATION_0 }
) {
viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
assertThat(viewModel.sensorBounds.value).isNotNull()
assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
assertThat(viewModel.sensorBounds.value.top)
.isEqualTo(sensorLocation.sensorLocationY)
}
}
/**
* {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for
* ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the
* correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds
* works correctly, tests for indicator placement in other rotations have been omitted.
*/
@Test
fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
testScope.runTest {
testWithDisplay(
deviceConfig = DeviceConfig.Y_ALIGNED,
isReverseDefaultRotation = true,
{ rotation = Surface.ROTATION_270 }
) {
viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation)
assertThat(viewModel.sensorBounds.value).isNotNull()
assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth)
assertThat(viewModel.sensorBounds.value.top)
.isEqualTo(sensorLocation.sensorLocationY)
}
}
}

View File

@@ -20,12 +20,16 @@ import android.hardware.biometrics.SensorLocationInternal
import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.biometrics.shared.model.SensorStrength
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isInitialized = _isInitialized.asStateFlow()
private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1) private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
override val sensorId = _sensorId.asStateFlow() override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
private val _strength: MutableStateFlow<SensorStrength> = private val _strength: MutableStateFlow<SensorStrength> =
MutableStateFlow(SensorStrength.CONVENIENCE) MutableStateFlow(SensorStrength.CONVENIENCE)
@@ -33,11 +37,12 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
private val _sensorType: MutableStateFlow<FingerprintSensorType> = private val _sensorType: MutableStateFlow<FingerprintSensorType> =
MutableStateFlow(FingerprintSensorType.UNKNOWN) MutableStateFlow(FingerprintSensorType.UNKNOWN)
override val sensorType = _sensorType.asStateFlow() override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> = private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT)) MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
override val sensorLocations = _sensorLocations.asStateFlow() override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
_sensorLocations.asStateFlow()
fun setProperties( fun setProperties(
sensorId: Int, sensorId: Int,
@@ -49,5 +54,6 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
_strength.value = strength _strength.value = strength
_sensorType.value = sensorType _sensorType.value = sensorType
_sensorLocations.value = sensorLocations _sensorLocations.value = sensorLocations
_isInitialized.value = true
} }
} }