diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 6856717653bd9052d6f7de0556ad2c03d18eec65..75e71e414262435f417434ace60c7c6f709621df 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -957,4 +957,15 @@ bouncer, lockscreen, shade, and quick settings. --> <bool name="config_sceneContainerFrameworkEnabled">true</bool> + + <!-- + Time in milliseconds the user has to touch the side FPS sensor to successfully authenticate + TODO(b/302332976) Get this value from the HAL if they can provide an API for it. + --> + <integer name="config_restToUnlockDuration">300</integer> + + <!-- + Width in pixels of the Side FPS sensor. + --> + <integer name="config_sfpsSensorWidth">200</integer> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt index a590dccd53180831fe47c3d15804baae7d85f1dc..b9b2fd8875d9f3051efefe96f42c465d4e89e6cc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/BiometricsDomainLayerModule.kt @@ -23,8 +23,6 @@ import com.android.systemui.biometrics.domain.interactor.LogContextInteractor import com.android.systemui.biometrics.domain.interactor.LogContextInteractorImpl import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl -import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor -import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl import com.android.systemui.dagger.SysUISingleton import dagger.Binds import dagger.Module @@ -49,10 +47,4 @@ interface BiometricsDomainLayerModule { @Binds @SysUISingleton fun bindsLogContextInteractor(impl: LogContextInteractorImpl): LogContextInteractor - - @Binds - @SysUISingleton - fun providesSideFpsOverlayInteractor( - impl: SideFpsOverlayInteractorImpl - ): SideFpsOverlayInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt index f36a3ec89e3f3ea07dd5acc05a1f123c3889d416..a317a068405514844fa0162e891c627e4b2d484a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/DisplayStateInteractor.kt @@ -54,6 +54,9 @@ interface DisplayStateInteractor { /** Current rotation of the display */ val currentRotation: StateFlow<DisplayRotation> + /** Display change event indicating a change to the given displayId has occurred. */ + val displayChanges: Flow<Int> + /** Called on configuration changes, used to keep the display state in sync */ fun onConfigurationChanged(newConfig: Configuration) } @@ -74,6 +77,8 @@ constructor( screenSizeFoldProvider = foldProvider } + override val displayChanges = displayRepository.displayChangeEvent + override val isFolded: Flow<Boolean> = conflatedCallbackFlow { val sendFoldStateUpdate = { state: Boolean -> 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 deleted file mode 100644 index 75ae061f8ccec1ab76f3167f21d3f9358eeb6887..0000000000000000000000000000000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt +++ /dev/null @@ -1,62 +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.domain.interactor - -import android.hardware.biometrics.SensorLocationInternal -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> - - /** Overlay offsets corresponding to given displayId. */ - val overlayOffsets: Flow<SensorLocationInternal> - - /** Called on display changes, used to keep the display state in sync */ - fun onDisplayChanged(displayId: String) -} - -@SysUISingleton -class SideFpsOverlayInteractorImpl -@Inject -constructor(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 onDisplayChanged(displayId: String) { - _displayId.value = displayId - } - - companion object { - private const val TAG = "SideFpsOverlayInteractorImpl" - } -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..f85203ea20767cbd3fc58b3c69b21111c7134101 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt @@ -0,0 +1,136 @@ +/* + * 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.domain.interactor + +import android.content.Context +import android.hardware.biometrics.SensorLocationInternal +import android.view.WindowManager +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository +import com.android.systemui.biometrics.domain.model.SideFpsSensorLocation +import com.android.systemui.biometrics.shared.model.DisplayRotation +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.isDefaultOrientation +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.Flags +import com.android.systemui.res.R +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class SideFpsSensorInteractor +@Inject +constructor( + private val context: Context, + fingerprintPropertyRepository: FingerprintPropertyRepository, + windowManager: WindowManager, + displayStateInteractor: DisplayStateInteractor, + featureFlags: FeatureFlagsClassic, +) { + + private val sensorForCurrentDisplay = + combine( + displayStateInteractor.displayChanges, + fingerprintPropertyRepository.sensorLocations, + ::Pair + ) + .map { (_, locations) -> locations[context.display?.uniqueId] } + .filterNotNull() + + val isAvailable: Flow<Boolean> = + fingerprintPropertyRepository.sensorType.map { it == FingerprintSensorType.POWER_BUTTON } + + val authenticationDuration: Flow<Long> = + flowOf(context.resources?.getInteger(R.integer.config_restToUnlockDuration)?.toLong() ?: 0L) + + val isProlongedTouchRequiredForAuthentication: Flow<Boolean> = + isAvailable.flatMapLatest { sfpsAvailable -> + if (sfpsAvailable) { + // todo (b/305236201) also add the settings check here. + flowOf(featureFlags.isEnabled(Flags.REST_TO_UNLOCK)) + } else { + flowOf(false) + } + } + + val sensorLocation: Flow<SideFpsSensorLocation> = + combine(displayStateInteractor.currentRotation, sensorForCurrentDisplay, ::Pair).map { + (rotation, sensorLocation: SensorLocationInternal) -> + val isSensorVerticalInDefaultOrientation = sensorLocation.sensorLocationY != 0 + // device dimensions in the current rotation + val size = windowManager.maximumWindowMetrics.bounds + val isDefaultOrientation = rotation.isDefaultOrientation() + // Width and height are flipped is device is not in rotation_0 or rotation_180 + // Flipping it to the width and height of the device in default orientation. + val displayWidth = if (isDefaultOrientation) size.width() else size.height() + val displayHeight = if (isDefaultOrientation) size.height() else size.width() + val sensorWidth = context.resources?.getInteger(R.integer.config_sfpsSensorWidth) ?: 0 + + val (sensorLeft, sensorTop) = + if (isSensorVerticalInDefaultOrientation) { + when (rotation) { + DisplayRotation.ROTATION_0 -> { + Pair(displayWidth, sensorLocation.sensorLocationY) + } + DisplayRotation.ROTATION_90 -> { + Pair(sensorLocation.sensorLocationY, 0) + } + DisplayRotation.ROTATION_180 -> { + Pair(0, displayHeight - sensorLocation.sensorLocationY - sensorWidth) + } + DisplayRotation.ROTATION_270 -> { + Pair( + displayHeight - sensorLocation.sensorLocationY - sensorWidth, + displayWidth + ) + } + } + } else { + when (rotation) { + DisplayRotation.ROTATION_0 -> { + Pair(sensorLocation.sensorLocationX, 0) + } + DisplayRotation.ROTATION_90 -> { + Pair(0, displayWidth - sensorLocation.sensorLocationX - sensorWidth) + } + DisplayRotation.ROTATION_180 -> { + Pair( + displayWidth - sensorLocation.sensorLocationX - sensorWidth, + displayHeight + ) + } + DisplayRotation.ROTATION_270 -> { + Pair(displayHeight, sensorLocation.sensorLocationX) + } + } + } + + SideFpsSensorLocation( + left = sensorLeft, + top = sensorTop, + width = sensorWidth, + isSensorVerticalInDefaultOrientation = isSensorVerticalInDefaultOrientation + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt new file mode 100644 index 0000000000000000000000000000000000000000..35f8e3bb461ff1152a33f360f2cc42beea457541 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/SideFpsSensorLocation.kt @@ -0,0 +1,31 @@ +/* + * 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.domain.model + +data class SideFpsSensorLocation( + /** Pixel offset from the left of the screen */ + val left: Int, + /** Pixel offset from the top of the screen */ + val top: Int, + /** Width in pixels of the SFPS sensor */ + val width: Int, + /** + * Whether the sensor is vertical when the device is in its default orientation (Rotation_0 or + * Rotation_180) + */ + val isSensorVerticalInDefaultOrientation: Boolean +) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/DisplayRotation.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/DisplayRotation.kt index 10a3e915fe806128d79d330fa05070dc8ec27740..336404c04dbfceda3c03e73080f5c9eb728f730a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/DisplayRotation.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/DisplayRotation.kt @@ -10,6 +10,9 @@ enum class DisplayRotation { ROTATION_270, } +fun DisplayRotation.isDefaultOrientation() = + this == DisplayRotation.ROTATION_0 || this == DisplayRotation.ROTATION_180 + /** Converts [Surface.Rotation] to corresponding [DisplayRotation] */ fun Int.toDisplayRotation(): DisplayRotation = when (this) { @@ -19,3 +22,12 @@ fun Int.toDisplayRotation(): DisplayRotation = Surface.ROTATION_270 -> DisplayRotation.ROTATION_270 else -> throw IllegalArgumentException("Invalid DisplayRotation value: $this") } + +/** Converts [DisplayRotation] to corresponding [Surface.Rotation] */ +fun DisplayRotation.toRotation(): Int = + when (this) { + DisplayRotation.ROTATION_0 -> Surface.ROTATION_0 + DisplayRotation.ROTATION_90 -> Surface.ROTATION_90 + DisplayRotation.ROTATION_180 -> Surface.ROTATION_180 + DisplayRotation.ROTATION_270 -> Surface.ROTATION_270 + } 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 deleted file mode 100644 index 712eef13421bf26b38814fdcc6b90d9983d9f2f2..0000000000000000000000000000000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt +++ /dev/null @@ -1,108 +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.domain.interactor - -import android.hardware.biometrics.SensorLocationInternal -import androidx.test.filters.SmallTest -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 -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.junit.MockitoJUnit - -@SmallTest -@RunWith(JUnit4::class) -class SideFpsOverlayInteractorTest : SysuiTestCase() { - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - private lateinit var testScope: TestScope - - private val fingerprintRepository = FakeFingerprintPropertyRepository() - - private lateinit var interactor: SideFpsOverlayInteractor - - @Before - fun setup() { - testScope = TestScope(StandardTestDispatcher()) - interactor = SideFpsOverlayInteractorImpl(fingerprintRepository) - } - - @Test - fun testOverlayOffsetUpdates() = - testScope.runTest { - 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 */ - ) - ) - ) - - val displayId by collectLastValue(interactor.displayId) - val offsets by collectLastValue(interactor.overlayOffsets) - - // 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.onDisplayChanged("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.onDisplayChanged("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) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..99501c426e0c1cb3df572a53289fb13f871ce459 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt @@ -0,0 +1,410 @@ +/* + * 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.domain.interactor + +import android.graphics.Rect +import android.hardware.biometrics.SensorLocationInternal +import android.hardware.display.DisplayManagerGlobal +import android.view.Display +import android.view.DisplayInfo +import android.view.WindowInsets +import android.view.WindowManager +import android.view.WindowMetrics +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.DisplayRotation +import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_0 +import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_180 +import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_270 +import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_90 +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.flags.FakeFeatureFlagsClassic +import com.android.systemui.flags.Flags.REST_TO_UNLOCK +import com.android.systemui.res.R +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +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.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.spy +import org.mockito.junit.MockitoJUnit + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class SideFpsSensorInteractorTest : SysuiTestCase() { + + @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + private lateinit var testScope: TestScope + + private val fingerprintRepository = FakeFingerprintPropertyRepository() + + private lateinit var underTest: SideFpsSensorInteractor + + @Mock private lateinit var windowManager: WindowManager + @Mock private lateinit var displayStateInteractor: DisplayStateInteractor + + private val contextDisplayInfo = DisplayInfo() + private val displayChangeEvent = MutableStateFlow(0) + private val currentRotation = MutableStateFlow(ROTATION_0) + + @Before + fun setup() { + testScope = TestScope(StandardTestDispatcher()) + mContext = spy(mContext) + + val displayManager = mock(DisplayManagerGlobal::class.java) + val resources = mContext.resources + whenever(mContext.display) + .thenReturn(Display(displayManager, 1, contextDisplayInfo, resources)) + whenever(displayStateInteractor.displayChanges).thenReturn(displayChangeEvent) + whenever(displayStateInteractor.currentRotation).thenReturn(currentRotation) + + contextDisplayInfo.uniqueId = "current-display" + + underTest = + SideFpsSensorInteractor( + mContext, + fingerprintRepository, + windowManager, + displayStateInteractor, + FakeFeatureFlagsClassic().apply { set(REST_TO_UNLOCK, true) } + ) + } + + @Test + fun testSfpsSensorAvailable() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isAvailable) + + setupFingerprint(FingerprintSensorType.POWER_BUTTON) + assertThat(isAvailable).isTrue() + + setupFingerprint(FingerprintSensorType.HOME_BUTTON) + assertThat(isAvailable).isFalse() + + setupFingerprint(FingerprintSensorType.REAR) + assertThat(isAvailable).isFalse() + + setupFingerprint(FingerprintSensorType.UDFPS_OPTICAL) + assertThat(isAvailable).isFalse() + + setupFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) + assertThat(isAvailable).isFalse() + + setupFingerprint(FingerprintSensorType.UNKNOWN) + assertThat(isAvailable).isFalse() + } + + @Test + fun authenticationDurationIsAvailableWhenSFPSSensorIsAvailable() = + testScope.runTest { + assertThat(collectLastValue(underTest.authenticationDuration)()) + .isEqualTo(context.resources.getInteger(R.integer.config_restToUnlockDuration)) + } + + @Test + fun verticalSensorLocationIsAdjustedToScreenPositionForRotation0() = + testScope.runTest { + /* + (0,0) (1000,0) + ------------------ + | ^^^^^ | (1000, 200) + | status bar || <--- start of sensor at Rotation_0 + | || <--- end of sensor + | | (1000, 300) + | | + ------------------ (1000, 800) + */ + setupFPLocationAndDisplaySize( + width = 1000, + height = 800, + rotation = ROTATION_0, + sensorLocationY = 200, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(1000) + assertThat(sensorLocation!!.top).isEqualTo(200) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(true) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + @Test + fun verticalSensorLocationIsAdjustedToScreenPositionForRotation270() = + testScope.runTest { + /* + (800,0) (800, 1000) + --------------------- + | | (600, 1000) + | < || <--- end of sensor at Rotation_270 + | < status bar || <--- start of sensor + | < | (500, 1000) + | < | + (0,0) --------------------- + */ + setupFPLocationAndDisplaySize( + width = 800, + height = 1000, + rotation = ROTATION_270, + sensorLocationY = 200, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(500) + assertThat(sensorLocation!!.top).isEqualTo(1000) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(true) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + @Test + fun verticalSensorLocationIsAdjustedToScreenPositionForRotation90() = + testScope.runTest { + /* + (0,0) + --------------------- + | | (200, 0) + | > || <--- end of sensor at Rotation_270 + | status bar > || <--- start of sensor + | > | (300, 0) + | > | + (800,1000) --------------------- + */ + setupFPLocationAndDisplaySize( + width = 800, + height = 1000, + rotation = ROTATION_90, + sensorLocationY = 200, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(200) + assertThat(sensorLocation!!.top).isEqualTo(0) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(true) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + @Test + fun verticalSensorLocationIsAdjustedToScreenPositionForRotation180() = + testScope.runTest { + /* + + (1000,800) --------------------- + | | (0, 600) + | || <--- end of sensor at Rotation_270 + | status bar || <--- start of sensor + | \/\/\/\/\/\/\/ | (0, 500) + | | + --------------------- (0,0) + */ + setupFPLocationAndDisplaySize( + width = 1000, + height = 800, + rotation = ROTATION_180, + sensorLocationY = 200, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(0) + assertThat(sensorLocation!!.top).isEqualTo(500) + } + + @Test + fun horizontalSensorLocationIsAdjustedToScreenPositionForRotation0() = + testScope.runTest { + /* + (0,0) (500,0) (600,0) (1000,0) + ____________===_________ + | | + | ^^^^^ | + | status bar | + | | + ------------------------ (1000, 800) + */ + setupFPLocationAndDisplaySize( + width = 1000, + height = 800, + rotation = ROTATION_0, + sensorLocationX = 500, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(500) + assertThat(sensorLocation!!.top).isEqualTo(0) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + @Test + fun horizontalSensorLocationIsAdjustedToScreenPositionForRotation90() = + testScope.runTest { + /* + (0,1000) (0,500) (0,400) (0,0) + ____________===_________ + | | + | > | + | status bar > | + | > | + (800, 1000) ------------------------ + */ + setupFPLocationAndDisplaySize( + width = 800, + height = 1000, + rotation = ROTATION_90, + sensorLocationX = 500, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(0) + assertThat(sensorLocation!!.top).isEqualTo(400) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + @Test + fun horizontalSensorLocationIsAdjustedToScreenPositionForRotation180() = + testScope.runTest { + /* + (1000, 800) (500, 800) (400, 800) (0,800) + ____________===_________ + | | + | | + | status bar | + | \/ \/ \/ \/ \/ \/ \/ | + ------------------------ (0,0) + */ + setupFPLocationAndDisplaySize( + width = 1000, + height = 800, + rotation = ROTATION_180, + sensorLocationX = 500, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(400) + assertThat(sensorLocation!!.top).isEqualTo(800) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + @Test + fun horizontalSensorLocationIsAdjustedToScreenPositionForRotation270() = + testScope.runTest { + /* + (800, 500) (800, 600) + (800, 0) ____________===_________ (800,1000) + | < | + | < | + | < status bar | + | < | + (0,0) ------------------------ + */ + setupFPLocationAndDisplaySize( + width = 800, + height = 1000, + rotation = ROTATION_270, + sensorLocationX = 500, + sensorWidth = 100, + ) + + val sensorLocation by collectLastValue(underTest.sensorLocation) + assertThat(sensorLocation!!.left).isEqualTo(800) + assertThat(sensorLocation!!.top).isEqualTo(500) + assertThat(sensorLocation!!.isSensorVerticalInDefaultOrientation).isEqualTo(false) + assertThat(sensorLocation!!.width).isEqualTo(100) + } + + private suspend fun TestScope.setupFPLocationAndDisplaySize( + width: Int, + height: Int, + sensorLocationX: Int = 0, + sensorLocationY: Int = 0, + rotation: DisplayRotation, + sensorWidth: Int + ) { + overrideResource(R.integer.config_sfpsSensorWidth, sensorWidth) + setupDisplayDimensions(width, height) + currentRotation.value = rotation + setupFingerprint(x = sensorLocationX, y = sensorLocationY, displayId = "expanded_display") + } + + private fun setupDisplayDimensions(displayWidth: Int, displayHeight: Int) { + whenever(windowManager.maximumWindowMetrics) + .thenReturn( + WindowMetrics( + Rect(0, 0, displayWidth, displayHeight), + mock(WindowInsets::class.java) + ) + ) + } + + private suspend fun TestScope.setupFingerprint( + fingerprintSensorType: FingerprintSensorType = FingerprintSensorType.POWER_BUTTON, + x: Int = 0, + y: Int = 0, + displayId: String = "display_id_1" + ) { + contextDisplayInfo.uniqueId = displayId + fingerprintRepository.setProperties( + sensorId = 1, + strength = SensorStrength.STRONG, + sensorType = fingerprintSensorType, + sensorLocations = + mapOf( + "someOtherDisplayId" to + SensorLocationInternal( + "someOtherDisplayId", + x + 100, + y + 100, + 0, + ), + displayId to + SensorLocationInternal( + displayId, + x, + y, + 0, + ) + ) + ) + // Emit a display change event, this happens whenever any display related change happens, + // rotation, active display changing etc, display switched off/on. + displayChangeEvent.emit(1) + + runCurrent() + } +}