Revert "FingerprintSensorProperties refactor - ui layer"

This reverts commit 3e86cd7fe0.

Reason for revert: side fps stopped animating & flickery behavior

Bug: 291952939
Change-Id: I0ab574741e10cf9e157978f9667a4ad424d9a60a
This commit is contained in:
Beverly Tai
2023-07-19 18:28:06 +00:00
parent 3e86cd7fe0
commit 3e0236df97
10 changed files with 423 additions and 716 deletions

View File

@@ -17,7 +17,12 @@ package com.android.systemui.biometrics
import android.app.ActivityTaskManager
import android.content.Context
import android.content.res.Configuration
import android.graphics.Color
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.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -28,23 +33,27 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController
import android.os.Handler
import android.util.Log
import android.util.RotationUtils
import android.view.Display
import android.view.DisplayInfo
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
import android.view.View.AccessibilityDelegate
import android.view.ViewPropertyAnimator
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.accessibility.AccessibilityEvent
import androidx.annotation.RawRes
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.systemui.Dumpable
import com.android.systemui.R
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.dagger.SysUISingleton
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 java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -78,7 +86,6 @@ constructor(
@Main private val mainExecutor: DelayableExecutor,
@Main private val handler: Handler,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
@Application private val scope: CoroutineScope,
dumpManager: DumpManager
) : Dumpable {
@@ -243,15 +250,105 @@ constructor(
private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
overlayView = view
SideFpsOverlayViewBinder.bind(
view = view,
viewModel = sideFpsOverlayViewModelFactory.get(),
overlayViewParams = overlayViewParams,
reason = reason,
context = context,
val display = context.display!!
// b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
display.getDisplayInfo(displayInfo)
val offsets =
sensorProps.getLocation(display.uniqueId).let { location ->
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
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?
@@ -276,12 +373,89 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo
private fun ActivityTaskManager.topClass(): String =
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 Display.isNaturalOrientation(): Boolean =
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,
displayManager: DisplayManager,
handler: Handler,

View File

@@ -16,11 +16,8 @@
package com.android.systemui.biometrics.data.repository
import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.SensorLocationInternal
import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -33,8 +30,10 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
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
/**
@@ -44,17 +43,22 @@ import kotlinx.coroutines.flow.shareIn
*/
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. */
val sensorId: Flow<Int>
val sensorId: StateFlow<Int>
/** 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.). */
val sensorType: Flow<FingerprintSensorType>
val sensorType: StateFlow<FingerprintSensorType>
/** The sensor location relative to each physical display. */
val sensorLocations: Flow<Map<String, SensorLocationInternal>>
val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
}
@SysUISingleton
@@ -62,10 +66,10 @@ class FingerprintPropertyRepositoryImpl
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
private val fingerprintManager: FingerprintManager?
private val fingerprintManager: FingerprintManager
) : FingerprintPropertyRepository {
private val props: Flow<FingerprintSensorPropertiesInternal> =
override val isInitialized: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -73,47 +77,45 @@ constructor(
sensors: List<FingerprintSensorPropertiesInternal>
) {
if (sensors.isNotEmpty()) {
trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
} else {
trySendWithFailureLogging(
DEFAULT_PROPS,
TAG,
"initialize with default properties"
)
setProperties(sensors[0])
trySendWithFailureLogging(true, TAG, "initialize properties")
}
}
}
fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
fingerprintManager.addAuthenticatorsRegisteredCallback(callback)
trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
awaitClose {}
}
.shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
override val sensorId: Flow<Int> = props.map { it.sensorId }
override val strength: Flow<SensorStrength> =
props.map { sensorStrengthIntToObject(it.sensorStrength) }
override val sensorType: Flow<FingerprintSensorType> =
props.map { sensorTypeIntToObject(it.sensorType) }
override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
props.map {
it.allLocations.associateBy { sensorLocationInternal ->
private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
private val _strength: MutableStateFlow<SensorStrength> =
MutableStateFlow(SensorStrength.CONVENIENCE)
override val strength = _strength.asStateFlow()
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
}
}
}
companion object {
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
import android.hardware.biometrics.SensorLocationInternal
import android.util.Log
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.dagger.SysUISingleton
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. */
interface SideFpsOverlayInteractor {
/** The displayId of the current display. */
val displayId: Flow<String>
/** The corresponding offsets based on different displayId. */
val overlayOffsets: Flow<SensorLocationInternal>
/** Update the displayId. */
fun changeDisplay(displayId: String?)
/** Get the corresponding offsets based on different displayId. */
fun getOverlayOffsets(displayId: String): SensorLocationInternal
}
@SysUISingleton
@@ -43,16 +35,14 @@ class SideFpsOverlayInteractorImpl
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
SideFpsOverlayInteractor {
private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
override val displayId: Flow<String> = _displayId.asStateFlow()
override val overlayOffsets: Flow<SensorLocationInternal> =
combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
offsets[displayId] ?: SensorLocationInternal.DEFAULT
override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
val offsets = fingerprintPropertyRepository.sensorLocations.value
return if (offsets.containsKey(displayId)) {
offsets[displayId]!!
} else {
Log.w(TAG, "No location specified for display: $displayId")
offsets[""]!!
}
override fun changeDisplay(displayId: String?) {
_displayId.value = displayId ?: ""
}
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.WindowInsets
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 androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -52,14 +55,9 @@ import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
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.domain.interactor.DisplayStateInteractor
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.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dump.DumpManager
@@ -101,7 +99,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
@SmallTest
@RoboPilotTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@TestableLooper.RunWithLooper
class SideFpsControllerTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@ class SideFpsControllerTest : SysuiTestCase() {
private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
private lateinit var displayStateInteractor: DisplayStateInteractor
private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel
private val fingerprintRepository = FakeFingerprintPropertyRepository()
private val executor = FakeExecutor(FakeSystemClock())
private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
@@ -163,15 +159,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
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(WindowManager::class.java, windowManager)
@@ -278,7 +265,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
handler,
alternateBouncerInteractor,
{ sideFpsOverlayViewModel },
TestCoroutineScope(),
dumpManager
)
@@ -697,6 +683,106 @@ class SideFpsControllerTest : SysuiTestCase() {
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
fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
// By default all those tests assume the side fps sensor is available.
@@ -709,6 +795,51 @@ class SideFpsControllerTest : SysuiTestCase() {
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)

View File

@@ -73,15 +73,10 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
@Test
fun initializeProperties() =
testScope.runTest {
val sensorId by collectLastValue(repository.sensorId)
val strength by collectLastValue(repository.strength)
val sensorType by collectLastValue(repository.sensorType)
val sensorLocations by collectLastValue(repository.sensorLocations)
val isInitialized = collectLastValue(repository.isInitialized)
// Assert default properties.
assertThat(sensorId).isEqualTo(-1)
assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
assertDefaultProperties()
assertThat(isInitialized()).isFalse()
val fingerprintProps =
listOf(
@@ -120,24 +115,31 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
assertThat(sensorId).isEqualTo(1)
assertThat(strength).isEqualTo(SensorStrength.STRONG)
assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
assertThat(repository.sensorId.value).isEqualTo(1)
assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
assertThat(sensorLocations?.size).isEqualTo(2)
assertThat(sensorLocations).containsKey("display_id_1")
with(sensorLocations?.get("display_id_1")!!) {
assertThat(repository.sensorLocations.value.size).isEqualTo(2)
assertThat(repository.sensorLocations.value).containsKey("display_id_1")
with(repository.sensorLocations.value["display_id_1"]!!) {
assertThat(displayId).isEqualTo("display_id_1")
assertThat(sensorLocationX).isEqualTo(100)
assertThat(sensorLocationY).isEqualTo(300)
assertThat(sensorRadius).isEqualTo(20)
}
assertThat(sensorLocations).containsKey("")
with(sensorLocations?.get("")!!) {
assertThat(repository.sensorLocations.value).containsKey("")
with(repository.sensorLocations.value[""]!!) {
assertThat(displayId).isEqualTo("")
assertThat(sensorLocationX).isEqualTo(540)
assertThat(sensorLocationY).isEqualTo(1636)
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.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -52,9 +51,8 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
}
@Test
fun testGetOverlayoffsets() =
fun testGetOverlayOffsets() =
testScope.runTest {
// Arrange.
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
)
)
// Act.
val offsets by collectLastValue(interactor.overlayOffsets)
val displayId by collectLastValue(interactor.displayId)
var offsets = interactor.getOverlayOffsets("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)
// Assert offsets of empty displayId.
assertThat(displayId).isEqualTo("")
assertThat(offsets?.displayId).isEqualTo("")
assertThat(offsets?.sensorLocationX).isEqualTo(540)
assertThat(offsets?.sensorLocationY).isEqualTo(1636)
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)
offsets = interactor.getOverlayOffsets("invalid_display_id")
assertThat(offsets.displayId).isEqualTo("")
assertThat(offsets.sensorLocationX).isEqualTo(540)
assertThat(offsets.sensorLocationY).isEqualTo(1636)
assertThat(offsets.sensorRadius).isEqualTo(130)
}
}

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