summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Beverly Tai <beverlyt@google.com> 2023-07-22 00:32:30 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2023-07-22 00:32:30 +0000
commit213e0ec1b80f14dc51de948bf686e7e6b3ab6670 (patch)
tree728f3a7d43afa6ecef1c969f5e5b8430c52e3ea9
parent37398b04adf7c44b7e8f3e3943058746c3b29cbb (diff)
parentf50bff6ed287ac7fb67a92a58bbd838079a58556 (diff)
Merge "Revert "Revert "Revert "FingerprintSensorProperties refactor - ui layer"""" into udc-qpr-dev
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt196
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt78
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt30
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt168
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt148
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt167
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt34
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt41
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt263
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt12
10 files changed, 422 insertions, 715 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 083e21fbdfba..37ce44488346 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -17,7 +17,12 @@ package com.android.systemui.biometrics
import android.app.ActivityTaskManager
import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Color
import android.graphics.PixelFormat
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
@@ -28,23 +33,27 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController
import android.os.Handler
import android.util.Log
+import android.util.RotationUtils
import android.view.Display
import android.view.DisplayInfo
import android.view.Gravity
import android.view.LayoutInflater
import android.view.Surface
import android.view.View
+import android.view.View.AccessibilityDelegate
import android.view.ViewPropertyAnimator
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.accessibility.AccessibilityEvent
+import androidx.annotation.RawRes
import com.airbnb.lottie.LottieAnimationView
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.model.KeyPath
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
-import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -55,7 +64,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.traceSection
import java.io.PrintWriter
import javax.inject.Inject
-import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -78,7 +86,6 @@ constructor(
@Main private val mainExecutor: DelayableExecutor,
@Main private val handler: Handler,
private val alternateBouncerInteractor: AlternateBouncerInteractor,
- private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>,
@Application private val scope: CoroutineScope,
dumpManager: DumpManager
) : Dumpable {
@@ -243,15 +250,105 @@ constructor(
private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
overlayView = view
- SideFpsOverlayViewBinder.bind(
- view = view,
- viewModel = sideFpsOverlayViewModelFactory.get(),
- overlayViewParams = overlayViewParams,
- reason = reason,
- context = context,
+ val display = context.display!!
+ // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
+ display.getDisplayInfo(displayInfo)
+ val offsets =
+ sensorProps.getLocation(display.uniqueId).let { location ->
+ if (location == null) {
+ Log.w(TAG, "No location specified for display: ${display.uniqueId}")
+ }
+ location ?: sensorProps.location
+ }
+ overlayOffsets = offsets
+
+ val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
+ view.rotation =
+ display.asSideFpsAnimationRotation(
+ offsets.isYAligned(),
+ getRotationFromDefault(displayInfo.rotation)
+ )
+ lottie.setAnimation(
+ display.asSideFpsAnimation(
+ offsets.isYAligned(),
+ getRotationFromDefault(displayInfo.rotation)
+ )
)
+ lottie.addLottieOnCompositionLoadedListener {
+ // Check that view is not stale, and that overlayView has not been hidden/removed
+ if (overlayView != null && overlayView == view) {
+ updateOverlayParams(display, it.bounds)
+ }
+ }
orientationReasonListener.reason = reason
+ lottie.addOverlayDynamicColor(context, reason)
+
+ /**
+ * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
+ * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
+ * in focus
+ */
+ view.setAccessibilityDelegate(
+ object : AccessibilityDelegate() {
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent
+ ): Boolean {
+ return if (
+ event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ ) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
+ )
}
+
+ @VisibleForTesting
+ fun updateOverlayParams(display: Display, bounds: Rect) {
+ val isNaturalOrientation = display.isNaturalOrientation()
+ val isDefaultOrientation =
+ if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
+ val size = windowManager.maximumWindowMetrics.bounds
+
+ val displayWidth = if (isDefaultOrientation) size.width() else size.height()
+ val displayHeight = if (isDefaultOrientation) size.height() else size.width()
+ val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
+ val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
+
+ val sensorBounds =
+ if (overlayOffsets.isYAligned()) {
+ Rect(
+ displayWidth - boundsWidth,
+ overlayOffsets.sensorLocationY,
+ displayWidth,
+ overlayOffsets.sensorLocationY + boundsHeight
+ )
+ } else {
+ Rect(
+ overlayOffsets.sensorLocationX,
+ 0,
+ overlayOffsets.sensorLocationX + boundsWidth,
+ boundsHeight
+ )
+ }
+
+ RotationUtils.rotateBounds(
+ sensorBounds,
+ Rect(0, 0, displayWidth, displayHeight),
+ getRotationFromDefault(display.rotation)
+ )
+
+ overlayViewParams.x = sensorBounds.left
+ overlayViewParams.y = sensorBounds.top
+
+ windowManager.updateViewLayout(overlayView, overlayViewParams)
+ }
+
+ private fun getRotationFromDefault(rotation: Int): Int =
+ if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
}
private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
@@ -276,12 +373,89 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo
private fun ActivityTaskManager.topClass(): String =
getTasks(1).firstOrNull()?.topActivity?.className ?: ""
+@RawRes
+private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
+ when (rotationFromDefault) {
+ Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+ Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
+ else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
+ }
+
+private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
+ when (rotationFromDefault) {
+ Surface.ROTATION_90 -> if (yAligned) 0f else 180f
+ Surface.ROTATION_180 -> 180f
+ Surface.ROTATION_270 -> if (yAligned) 180f else 0f
+ else -> 0f
+ }
+
private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
private fun Display.isNaturalOrientation(): Boolean =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
-public class OrientationReasonListener(
+private fun LottieAnimationView.addOverlayDynamicColor(
+ context: Context,
+ @BiometricOverlayConstants.ShowReason reason: Int
+) {
+ fun update() {
+ val isKeyguard = reason == REASON_AUTH_KEYGUARD
+ if (isKeyguard) {
+ val color =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorPrimaryFixed
+ )
+ val outerRimColor =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorPrimaryFixedDim
+ )
+ val chevronFill =
+ com.android.settingslib.Utils.getColorAttrDefaultColor(
+ context,
+ com.android.internal.R.attr.materialColorOnPrimaryFixed
+ )
+ addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
+ }
+ addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
+ }
+ addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
+ }
+ } else {
+ if (!isDarkMode(context)) {
+ addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
+ }
+ }
+ for (key in listOf(".blue600", ".blue400")) {
+ addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
+ PorterDuffColorFilter(
+ context.getColor(R.color.settingslib_color_blue400),
+ PorterDuff.Mode.SRC_ATOP
+ )
+ }
+ }
+ }
+ }
+
+ if (composition != null) {
+ update()
+ } else {
+ addLottieOnCompositionLoadedListener { update() }
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
+ return darkMode == Configuration.UI_MODE_NIGHT_YES
+}
+
+@VisibleForTesting
+class OrientationReasonListener(
context: Context,
displayManager: DisplayManager,
handler: Handler,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
index efbde4c5985b..efc92ad3b4c8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt
@@ -16,11 +16,8 @@
package com.android.systemui.biometrics.data.repository
-import android.hardware.biometrics.ComponentInfoInternal
import android.hardware.biometrics.SensorLocationInternal
-import android.hardware.biometrics.SensorProperties
import android.hardware.fingerprint.FingerprintManager
-import android.hardware.fingerprint.FingerprintSensorProperties
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
@@ -33,8 +30,10 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.shareIn
/**
@@ -44,17 +43,22 @@ import kotlinx.coroutines.flow.shareIn
*/
interface FingerprintPropertyRepository {
+ /**
+ * If the repository is initialized or not. Other properties are defaults until this is true.
+ */
+ val isInitialized: Flow<Boolean>
+
/** The id of fingerprint sensor. */
- val sensorId: Flow<Int>
+ val sensorId: StateFlow<Int>
/** The security strength of sensor (convenience, weak, strong). */
- val strength: Flow<SensorStrength>
+ val strength: StateFlow<SensorStrength>
/** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */
- val sensorType: Flow<FingerprintSensorType>
+ val sensorType: StateFlow<FingerprintSensorType>
/** The sensor location relative to each physical display. */
- val sensorLocations: Flow<Map<String, SensorLocationInternal>>
+ val sensorLocations: StateFlow<Map<String, SensorLocationInternal>>
}
@SysUISingleton
@@ -62,10 +66,10 @@ class FingerprintPropertyRepositoryImpl
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- private val fingerprintManager: FingerprintManager?
+ private val fingerprintManager: FingerprintManager?,
) : FingerprintPropertyRepository {
- private val props: Flow<FingerprintSensorPropertiesInternal> =
+ override val isInitialized: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
object : IFingerprintAuthenticatorsRegisteredCallback.Stub() {
@@ -73,47 +77,45 @@ constructor(
sensors: List<FingerprintSensorPropertiesInternal>
) {
if (sensors.isNotEmpty()) {
- trySendWithFailureLogging(sensors[0], TAG, "initialize properties")
- } else {
- trySendWithFailureLogging(
- DEFAULT_PROPS,
- TAG,
- "initialize with default properties"
- )
+ setProperties(sensors[0])
+ trySendWithFailureLogging(true, TAG, "initialize properties")
}
}
}
fingerprintManager?.addAuthenticatorsRegisteredCallback(callback)
- trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties")
+ trySendWithFailureLogging(false, TAG, "initial value defaulting to false")
awaitClose {}
}
.shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1)
- override val sensorId: Flow<Int> = props.map { it.sensorId }
- override val strength: Flow<SensorStrength> =
- props.map { sensorStrengthIntToObject(it.sensorStrength) }
- override val sensorType: Flow<FingerprintSensorType> =
- props.map { sensorTypeIntToObject(it.sensorType) }
- override val sensorLocations: Flow<Map<String, SensorLocationInternal>> =
- props.map {
- it.allLocations.associateBy { sensorLocationInternal ->
+ private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
+ override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
+
+ private val _strength: MutableStateFlow<SensorStrength> =
+ MutableStateFlow(SensorStrength.CONVENIENCE)
+ override val strength = _strength.asStateFlow()
+
+ private val _sensorType: MutableStateFlow<FingerprintSensorType> =
+ MutableStateFlow(FingerprintSensorType.UNKNOWN)
+ override val sensorType = _sensorType.asStateFlow()
+
+ private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
+ MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
+ override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+ _sensorLocations.asStateFlow()
+
+ private fun setProperties(prop: FingerprintSensorPropertiesInternal) {
+ _sensorId.value = prop.sensorId
+ _strength.value = sensorStrengthIntToObject(prop.sensorStrength)
+ _sensorType.value = sensorTypeIntToObject(prop.sensorType)
+ _sensorLocations.value =
+ prop.allLocations.associateBy { sensorLocationInternal ->
sensorLocationInternal.displayId
}
- }
+ }
companion object {
private const val TAG = "FingerprintPropertyRepositoryImpl"
- private val DEFAULT_PROPS =
- FingerprintSensorPropertiesInternal(
- -1 /* sensorId */,
- SensorProperties.STRENGTH_CONVENIENCE,
- 0 /* maxEnrollmentsPerUser */,
- listOf<ComponentInfoInternal>(),
- FingerprintSensorProperties.TYPE_UNKNOWN,
- false /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT)
- )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
index 37f39cb5fe0e..aa85e5f3b21a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt
@@ -17,24 +17,16 @@
package com.android.systemui.biometrics.domain.interactor
import android.hardware.biometrics.SensorLocationInternal
+import android.util.Log
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
import com.android.systemui.dagger.SysUISingleton
import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
/** Business logic for SideFps overlay offsets. */
interface SideFpsOverlayInteractor {
- /** The displayId of the current display. */
- val displayId: Flow<String>
- /** The corresponding offsets based on different displayId. */
- val overlayOffsets: Flow<SensorLocationInternal>
-
- /** Update the displayId. */
- fun changeDisplay(displayId: String?)
+ /** Get the corresponding offsets based on different displayId. */
+ fun getOverlayOffsets(displayId: String): SensorLocationInternal
}
@SysUISingleton
@@ -43,16 +35,14 @@ class SideFpsOverlayInteractorImpl
constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) :
SideFpsOverlayInteractor {
- private val _displayId: MutableStateFlow<String> = MutableStateFlow("")
- override val displayId: Flow<String> = _displayId.asStateFlow()
-
- override val overlayOffsets: Flow<SensorLocationInternal> =
- combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets ->
- offsets[displayId] ?: SensorLocationInternal.DEFAULT
+ override fun getOverlayOffsets(displayId: String): SensorLocationInternal {
+ val offsets = fingerprintPropertyRepository.sensorLocations.value
+ return if (offsets.containsKey(displayId)) {
+ offsets[displayId]!!
+ } else {
+ Log.w(TAG, "No location specified for display: $displayId")
+ offsets[""]!!
}
-
- override fun changeDisplay(displayId: String?) {
- _displayId.value = displayId ?: ""
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
deleted file mode 100644
index 0409519c9816..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ /dev/null
@@ -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
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
deleted file mode 100644
index e938b4efb68c..000000000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ /dev/null
@@ -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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
index ecc776b98c6c..3169b091217b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt
@@ -44,6 +44,9 @@ import android.view.View
import android.view.ViewPropertyAnimator
import android.view.WindowInsets
import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
import android.view.WindowMetrics
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -53,14 +56,9 @@ import com.android.systemui.R
import com.android.systemui.RoboPilotTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestableContext
-import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
-import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl
-import com.android.systemui.biometrics.shared.model.FingerprintSensorType
-import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.dump.DumpManager
@@ -101,7 +99,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
@SmallTest
@RoboPilotTest
@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
class SideFpsControllerTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@ class SideFpsControllerTest : SysuiTestCase() {
private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
private lateinit var displayStateInteractor: DisplayStateInteractor
- private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel
- private val fingerprintRepository = FakeFingerprintPropertyRepository()
private val executor = FakeExecutor(FakeSystemClock())
private val rearDisplayStateRepository = FakeRearDisplayStateRepository()
@@ -163,15 +159,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
rearDisplayStateRepository
)
- sideFpsOverlayViewModel =
- SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository))
-
- fingerprintRepository.setProperties(
- sensorId = 1,
- strength = SensorStrength.STRONG,
- sensorType = FingerprintSensorType.REAR,
- sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0))
- )
context.addMockSystemService(DisplayManager::class.java, displayManager)
context.addMockSystemService(WindowManager::class.java, windowManager)
@@ -278,7 +265,6 @@ class SideFpsControllerTest : SysuiTestCase() {
executor,
handler,
alternateBouncerInteractor,
- { sideFpsOverlayViewModel },
TestCoroutineScope(),
dumpManager
)
@@ -697,6 +683,106 @@ class SideFpsControllerTest : SysuiTestCase() {
verify(windowManager).removeView(any())
}
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForXAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.X_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
+ * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
+ * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
+ * in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_0() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = false,
+ { rotation = Surface.ROTATION_0 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
+ /**
+ * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
+ * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
+ * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
+ * correctly, tests for indicator placement in other rotations have been omitted.
+ */
+ @Test
+ fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
+ testWithDisplay(
+ deviceConfig = DeviceConfig.Y_ALIGNED,
+ isReverseDefaultRotation = true,
+ { rotation = Surface.ROTATION_270 }
+ ) {
+ sideFpsController.overlayOffsets = sensorLocation
+
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+ assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
+ }
+
@Test
fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
// By default all those tests assume the side fps sensor is available.
@@ -709,6 +795,51 @@ class SideFpsControllerTest : SysuiTestCase() {
assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
}
+
+ @Test
+ fun testLayoutParams_isKeyguardDialogType() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpType = overlayViewParamsCaptor.value.type
+
+ assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue()
+ }
+
+ @Test
+ fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+ assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
+ }
+
+ @Test
+ fun testLayoutParams_hasTrustedOverlayWindowFlag() =
+ testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
+ sideFpsController.overlayOffsets = sensorLocation
+ sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
+ overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+ executor.runAllReady()
+
+ verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+ val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+ assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
+ }
}
private fun insetsForSmallNavbar() = insetsWithBottom(60)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
index ea2561594793..239e317b92f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt
@@ -73,15 +73,10 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
@Test
fun initializeProperties() =
testScope.runTest {
- val sensorId by collectLastValue(repository.sensorId)
- val strength by collectLastValue(repository.strength)
- val sensorType by collectLastValue(repository.sensorType)
- val sensorLocations by collectLastValue(repository.sensorLocations)
+ val isInitialized = collectLastValue(repository.isInitialized)
- // Assert default properties.
- assertThat(sensorId).isEqualTo(-1)
- assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE)
- assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN)
+ assertDefaultProperties()
+ assertThat(isInitialized()).isFalse()
val fingerprintProps =
listOf(
@@ -120,24 +115,31 @@ class FingerprintRepositoryImplTest : SysuiTestCase() {
fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps)
- assertThat(sensorId).isEqualTo(1)
- assertThat(strength).isEqualTo(SensorStrength.STRONG)
- assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR)
+ assertThat(repository.sensorId.value).isEqualTo(1)
+ assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG)
+ assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR)
- assertThat(sensorLocations?.size).isEqualTo(2)
- assertThat(sensorLocations).containsKey("display_id_1")
- with(sensorLocations?.get("display_id_1")!!) {
+ assertThat(repository.sensorLocations.value.size).isEqualTo(2)
+ assertThat(repository.sensorLocations.value).containsKey("display_id_1")
+ with(repository.sensorLocations.value["display_id_1"]!!) {
assertThat(displayId).isEqualTo("display_id_1")
assertThat(sensorLocationX).isEqualTo(100)
assertThat(sensorLocationY).isEqualTo(300)
assertThat(sensorRadius).isEqualTo(20)
}
- assertThat(sensorLocations).containsKey("")
- with(sensorLocations?.get("")!!) {
+ assertThat(repository.sensorLocations.value).containsKey("")
+ with(repository.sensorLocations.value[""]!!) {
assertThat(displayId).isEqualTo("")
assertThat(sensorLocationX).isEqualTo(540)
assertThat(sensorLocationY).isEqualTo(1636)
assertThat(sensorRadius).isEqualTo(130)
}
+ assertThat(isInitialized()).isTrue()
}
+
+ private fun assertDefaultProperties() {
+ assertThat(repository.sensorId.value).isEqualTo(-1)
+ assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE)
+ assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
index 896f9b114679..fd96cf45504b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt
@@ -22,7 +22,6 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
-import com.android.systemui.coroutines.collectLastValue
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
@@ -52,9 +51,8 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
}
@Test
- fun testGetOverlayoffsets() =
+ fun testGetOverlayOffsets() =
testScope.runTest {
- // Arrange.
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() {
)
)
- // Act.
- val offsets by collectLastValue(interactor.overlayOffsets)
- val displayId by collectLastValue(interactor.displayId)
+ var offsets = interactor.getOverlayOffsets("display_id_1")
+ assertThat(offsets.displayId).isEqualTo("display_id_1")
+ assertThat(offsets.sensorLocationX).isEqualTo(100)
+ assertThat(offsets.sensorLocationY).isEqualTo(300)
+ assertThat(offsets.sensorRadius).isEqualTo(20)
- // Assert offsets of empty displayId.
- assertThat(displayId).isEqualTo("")
- assertThat(offsets?.displayId).isEqualTo("")
- assertThat(offsets?.sensorLocationX).isEqualTo(540)
- assertThat(offsets?.sensorLocationY).isEqualTo(1636)
- assertThat(offsets?.sensorRadius).isEqualTo(130)
-
- // Offsets should be updated correctly.
- interactor.changeDisplay("display_id_1")
- assertThat(displayId).isEqualTo("display_id_1")
- assertThat(offsets?.displayId).isEqualTo("display_id_1")
- assertThat(offsets?.sensorLocationX).isEqualTo(100)
- assertThat(offsets?.sensorLocationY).isEqualTo(300)
- assertThat(offsets?.sensorRadius).isEqualTo(20)
-
- // Should return default offset when the displayId is invalid.
- interactor.changeDisplay("invalid_display_id")
- assertThat(displayId).isEqualTo("invalid_display_id")
- assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId)
- assertThat(offsets?.sensorLocationX)
- .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX)
- assertThat(offsets?.sensorLocationY)
- .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY)
- assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius)
+ offsets = interactor.getOverlayOffsets("invalid_display_id")
+ assertThat(offsets.displayId).isEqualTo("")
+ assertThat(offsets.sensorLocationX).isEqualTo(540)
+ assertThat(offsets.sensorLocationY).isEqualTo(1636)
+ assertThat(offsets.sensorRadius).isEqualTo(130)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
deleted file mode 100644
index a8593216e22a..000000000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
index 0c5e43809fab..2362a5241244 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt
@@ -20,12 +20,16 @@ import android.hardware.biometrics.SensorLocationInternal
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
+ private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val isInitialized = _isInitialized.asStateFlow()
+
private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1)
- override val sensorId = _sensorId.asStateFlow()
+ override val sensorId: StateFlow<Int> = _sensorId.asStateFlow()
private val _strength: MutableStateFlow<SensorStrength> =
MutableStateFlow(SensorStrength.CONVENIENCE)
@@ -33,11 +37,12 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
private val _sensorType: MutableStateFlow<FingerprintSensorType> =
MutableStateFlow(FingerprintSensorType.UNKNOWN)
- override val sensorType = _sensorType.asStateFlow()
+ override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow()
private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> =
MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT))
- override val sensorLocations = _sensorLocations.asStateFlow()
+ override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> =
+ _sensorLocations.asStateFlow()
fun setProperties(
sensorId: Int,
@@ -49,5 +54,6 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository {
_strength.value = strength
_sensorType.value = sensorType
_sensorLocations.value = sensorLocations
+ _isInitialized.value = true
}
}