diff options
| author | 2023-07-22 00:32:30 +0000 | |
|---|---|---|
| committer | 2023-07-22 00:32:30 +0000 | |
| commit | 213e0ec1b80f14dc51de948bf686e7e6b3ab6670 (patch) | |
| tree | 728f3a7d43afa6ecef1c969f5e5b8430c52e3ea9 | |
| parent | 37398b04adf7c44b7e8f3e3943058746c3b29cbb (diff) | |
| parent | f50bff6ed287ac7fb67a92a58bbd838079a58556 (diff) | |
Merge "Revert "Revert "Revert "FingerprintSensorProperties refactor - ui layer"""" into udc-qpr-dev
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 } } |