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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user