Merge "FingerprintSensorProperties refactor - ui layer" into udc-qpr-dev
This commit is contained in:
@@ -17,12 +17,7 @@ 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
|
||||
@@ -33,27 +28,23 @@ 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
|
||||
@@ -64,6 +55,7 @@ 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
|
||||
|
||||
@@ -86,6 +78,7 @@ 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 {
|
||||
@@ -250,105 +243,15 @@ constructor(
|
||||
private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
|
||||
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
|
||||
overlayView = view
|
||||
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)
|
||||
)
|
||||
SideFpsOverlayViewBinder.bind(
|
||||
view = view,
|
||||
viewModel = sideFpsOverlayViewModelFactory.get(),
|
||||
overlayViewParams = overlayViewParams,
|
||||
reason = reason,
|
||||
context = context,
|
||||
)
|
||||
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?
|
||||
@@ -373,89 +276,12 @@ 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
|
||||
|
||||
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(
|
||||
public class OrientationReasonListener(
|
||||
context: Context,
|
||||
displayManager: DisplayManager,
|
||||
handler: Handler,
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
|
||||
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
|
||||
@@ -30,10 +33,8 @@ 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.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
|
||||
/**
|
||||
@@ -43,22 +44,17 @@ 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: StateFlow<Int>
|
||||
val sensorId: Flow<Int>
|
||||
|
||||
/** The security strength of sensor (convenience, weak, strong). */
|
||||
val strength: StateFlow<SensorStrength>
|
||||
val strength: Flow<SensorStrength>
|
||||
|
||||
/** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
|
||||
val sensorType: StateFlow<FingerprintSensorType>
|
||||
val sensorType: Flow<FingerprintSensorType>
|
||||
|
||||
/** The sensor location relative to each physical display. */
|
||||
val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
|
||||
val sensorLocations: Flow<Map<String, SensorLocationInternal>>
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
@@ -66,10 +62,10 @@ class FingerprintPropertyRepositoryImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@Application private val applicationScope: CoroutineScope,
|
||||
private val fingerprintManager: FingerprintManager
|
||||
private val fingerprintManager: FingerprintManager?
|
||||
) : FingerprintPropertyRepository {
|
||||
|
||||
override val isInitialized: Flow<Boolean> =
|
||||
private val props: Flow<FingerprintSensorPropertiesInternal> =
|
||||
conflatedCallbackFlow {
|
||||
val callback =
|
||||
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
|
||||
@@ -77,45 +73,47 @@ constructor(
|
||||
sensors: List<FingerprintSensorPropertiesInternal>
|
||||
) {
|
||||
if (sensors.isNotEmpty()) {
|
||||
setProperties(sensors[0])
|
||||
trySendWithFailureLogging(true, TAG, "initialize properties")
|
||||
trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
|
||||
} else {
|
||||
trySendWithFailureLogging(
|
||||
DEFAULT_PROPS,
|
||||
TAG,
|
||||
"initialize with default properties"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
fingerprintManager.addAuthenticatorsRegisteredCallback(callback)
|
||||
trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
|
||||
fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
|
||||
trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
|
||||
awaitClose {}
|
||||
}
|
||||
.shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
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 ->
|
||||
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 ->
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,24 @@
|
||||
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>
|
||||
|
||||
/** Get the corresponding offsets based on different displayId. */
|
||||
fun getOverlayOffsets(displayId: String): SensorLocationInternal
|
||||
/** The corresponding offsets based on different displayId. */
|
||||
val overlayOffsets: Flow<SensorLocationInternal>
|
||||
|
||||
/** Update the displayId. */
|
||||
fun changeDisplay(displayId: String?)
|
||||
}
|
||||
|
||||
@SysUISingleton
|
||||
@@ -35,14 +43,16 @@ class SideFpsOverlayInteractorImpl
|
||||
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
|
||||
SideFpsOverlayInteractor {
|
||||
|
||||
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[""]!!
|
||||
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 changeDisplay(displayId: String?) {
|
||||
_displayId.value = displayId ?: ""
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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,9 +44,6 @@ 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
|
||||
@@ -55,9 +52,14 @@ 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
|
||||
@@ -99,7 +101,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
|
||||
@SmallTest
|
||||
@RoboPilotTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@TestableLooper.RunWithLooper
|
||||
@TestableLooper.RunWithLooper(setAsMainLooper = true)
|
||||
class SideFpsControllerTest : SysuiTestCase() {
|
||||
|
||||
@JvmField @Rule var rule = MockitoJUnit.rule()
|
||||
@@ -118,6 +120,8 @@ 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()
|
||||
@@ -159,6 +163,15 @@ 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)
|
||||
@@ -265,6 +278,7 @@ class SideFpsControllerTest : SysuiTestCase() {
|
||||
executor,
|
||||
handler,
|
||||
alternateBouncerInteractor,
|
||||
{ sideFpsOverlayViewModel },
|
||||
TestCoroutineScope(),
|
||||
dumpManager
|
||||
)
|
||||
@@ -683,106 +697,6 @@ 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.
|
||||
@@ -795,51 +709,6 @@ 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)
|
||||
|
||||
@@ -73,10 +73,15 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun initializeProperties() =
|
||||
testScope.runTest {
|
||||
val isInitialized = collectLastValue(repository.isInitialized)
|
||||
val sensorId by collectLastValue(repository.sensorId)
|
||||
val strength by collectLastValue(repository.strength)
|
||||
val sensorType by collectLastValue(repository.sensorType)
|
||||
val sensorLocations by collectLastValue(repository.sensorLocations)
|
||||
|
||||
assertDefaultProperties()
|
||||
assertThat(isInitialized()).isFalse()
|
||||
// Assert default properties.
|
||||
assertThat(sensorId).isEqualTo(-1)
|
||||
assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
|
||||
assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
|
||||
|
||||
val fingerprintProps =
|
||||
listOf(
|
||||
@@ -115,31 +120,24 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
|
||||
|
||||
fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
|
||||
|
||||
assertThat(repository.sensorId.value).isEqualTo(1)
|
||||
assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
|
||||
assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
|
||||
assertThat(sensorId).isEqualTo(1)
|
||||
assertThat(strength).isEqualTo(SensorStrength.STRONG)
|
||||
assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
|
||||
|
||||
assertThat(repository.sensorLocations.value.size).isEqualTo(2)
|
||||
assertThat(repository.sensorLocations.value).containsKey("display_id_1")
|
||||
with(repository.sensorLocations.value["display_id_1"]!!) {
|
||||
assertThat(sensorLocations?.size).isEqualTo(2)
|
||||
assertThat(sensorLocations).containsKey("display_id_1")
|
||||
with(sensorLocations?.get("display_id_1")!!) {
|
||||
assertThat(displayId).isEqualTo("display_id_1")
|
||||
assertThat(sensorLocationX).isEqualTo(100)
|
||||
assertThat(sensorLocationY).isEqualTo(300)
|
||||
assertThat(sensorRadius).isEqualTo(20)
|
||||
}
|
||||
assertThat(repository.sensorLocations.value).containsKey("")
|
||||
with(repository.sensorLocations.value[""]!!) {
|
||||
assertThat(sensorLocations).containsKey("")
|
||||
with(sensorLocations?.get("")!!) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ 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
|
||||
@@ -51,8 +52,9 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetOverlayOffsets() =
|
||||
fun testGetOverlayoffsets() =
|
||||
testScope.runTest {
|
||||
// Arrange.
|
||||
fingerprintRepository.setProperties(
|
||||
sensorId = 1,
|
||||
strength = SensorStrength.STRONG,
|
||||
@@ -76,16 +78,33 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
// Act.
|
||||
val offsets by collectLastValue(interactor.overlayOffsets)
|
||||
val displayId by collectLastValue(interactor.displayId)
|
||||
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* 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,16 +20,12 @@ 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: StateFlow<Int> = _sensorId.asStateFlow()
|
||||
override val sensorId = _sensorId.asStateFlow()
|
||||
|
||||
private val _strength: MutableStateFlow<SensorStrength> =
|
||||
MutableStateFlow(SensorStrength.CONVENIENCE)
|
||||
@@ -37,12 +33,11 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
|
||||
|
||||
private val _sensorType: MutableStateFlow<FingerprintSensorType> =
|
||||
MutableStateFlow(FingerprintSensorType.UNKNOWN)
|
||||
override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
|
||||
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()
|
||||
override val sensorLocations = _sensorLocations.asStateFlow()
|
||||
|
||||
fun setProperties(
|
||||
sensorId: Int,
|
||||
@@ -54,6 +49,5 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
|
||||
_strength.value = strength
|
||||
_sensorType.value = sensorType
|
||||
_sensorLocations.value = sensorLocations
|
||||
_isInitialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user