blob: b9a952aba2649078da1b89ef8e0c5dfcdcbdb803 [file] [log] [blame]
/*
* Copyright (C) 2021 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
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
import android.provider.Settings
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.Surface
import android.view.Surface.Rotation
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.statusbar.LockscreenShadeTransitionController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.phone.SystemUIDialogManager
import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
private const val REQUEST_ID = 2L
// Dimensions for the current display resolution.
private const val DISPLAY_WIDTH = 1080
private const val DISPLAY_HEIGHT = 1920
private const val SENSOR_WIDTH = 30
private const val SENSOR_HEIGHT = 60
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
class UdfpsControllerOverlayTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule()
@Mock private lateinit var fingerprintManager: FingerprintManager
@Mock private lateinit var inflater: LayoutInflater
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var accessibilityManager: AccessibilityManager
@Mock private lateinit var statusBarStateController: StatusBarStateController
@Mock private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var dialogManager: SystemUIDialogManager
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var transitionController: LockscreenShadeTransitionController
@Mock private lateinit var configurationController: ConfigurationController
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var unlockedScreenOffAnimationController:
UnlockedScreenOffAnimationController
@Mock private lateinit var udfpsDisplayMode: UdfpsDisplayModeProvider
@Mock private lateinit var secureSettings: SecureSettings
@Mock private lateinit var controllerCallback: IUdfpsOverlayControllerCallback
@Mock private lateinit var udfpsController: UdfpsController
@Mock private lateinit var udfpsView: UdfpsView
@Mock private lateinit var udfpsEnrollView: UdfpsEnrollView
@Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
@Mock private lateinit var featureFlags: FeatureFlags
@Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
@Mock private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
@Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
private lateinit var controllerOverlay: UdfpsControllerOverlay
@Before
fun setup() {
context.orCreateTestableResources.addOverride(R.integer.config_udfpsEnrollProgressBar, 20)
whenever(inflater.inflate(R.layout.udfps_view, null, false))
.thenReturn(udfpsView)
whenever(inflater.inflate(R.layout.udfps_enroll_view, null))
.thenReturn(udfpsEnrollView)
whenever(inflater.inflate(R.layout.udfps_bp_view, null))
.thenReturn(mock(UdfpsBpView::class.java))
whenever(inflater.inflate(R.layout.udfps_keyguard_view, null))
.thenReturn(mock(UdfpsKeyguardView::class.java))
whenever(inflater.inflate(R.layout.udfps_fpm_empty_view, null))
.thenReturn(mock(UdfpsFpmEmptyView::class.java))
whenever(udfpsEnrollView.context).thenReturn(context)
}
private fun withReason(
@ShowReason reason: Int,
isDebuggable: Boolean = false,
block: () -> Unit
) {
controllerOverlay = UdfpsControllerOverlay(
context, fingerprintManager, inflater, windowManager, accessibilityManager,
statusBarStateController, shadeExpansionStateManager, statusBarKeyguardViewManager,
keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
configurationController, keyguardStateController, unlockedScreenOffAnimationController,
udfpsDisplayMode, secureSettings, REQUEST_ID, reason,
controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
primaryBouncerInteractor, alternateBouncerInteractor, isDebuggable
)
block()
}
@Test
fun showUdfpsOverlay_bp() = withReason(REASON_AUTH_BP) { showUdfpsOverlay() }
@Test
fun showUdfpsOverlay_keyguard() = withReason(REASON_AUTH_KEYGUARD) { showUdfpsOverlay() }
@Test
fun showUdfpsOverlay_settings() = withReason(REASON_AUTH_SETTINGS) { showUdfpsOverlay() }
@Test
fun showUdfpsOverlay_locate() = withReason(REASON_ENROLL_FIND_SENSOR) {
showUdfpsOverlay(isEnrollUseCase = true)
}
@Test
fun showUdfpsOverlay_locate_withEnrollmentUiRemoved() {
Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
withReason(REASON_ENROLL_FIND_SENSOR, isDebuggable = true) {
showUdfpsOverlay(isEnrollUseCase = false)
}
Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
}
@Test
fun showUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) {
showUdfpsOverlay(isEnrollUseCase = true)
}
@Test
fun showUdfpsOverlay_enroll_withEnrollmentUiRemoved() {
Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 1)
withReason(REASON_ENROLL_ENROLLING, isDebuggable = true) {
showUdfpsOverlay(isEnrollUseCase = false)
}
Settings.Global.putInt(mContext.contentResolver, SETTING_REMOVE_ENROLLMENT_UI, 0)
}
@Test
fun showUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { showUdfpsOverlay() }
private fun withRotation(@Rotation rotation: Int, block: () -> Unit) {
// Sensor that's in the top left corner of the display in natural orientation.
val sensorBounds = Rect(0, 0, SENSOR_WIDTH, SENSOR_HEIGHT)
overlayParams = UdfpsOverlayParams(
sensorBounds,
sensorBounds,
DISPLAY_WIDTH,
DISPLAY_HEIGHT,
scaleFactor = 1f,
rotation
)
block()
}
@Test
fun showUdfpsOverlay_withRotation0() = withRotation(Surface.ROTATION_0) {
withReason(REASON_AUTH_BP) {
controllerOverlay.show(udfpsController, overlayParams)
verify(windowManager).addView(
eq(controllerOverlay.overlayView),
layoutParamsCaptor.capture()
)
// ROTATION_0 is the native orientation. Sensor should stay in the top left corner.
val lp = layoutParamsCaptor.value
assertThat(lp.x).isEqualTo(0)
assertThat(lp.y).isEqualTo(0)
assertThat(lp.width).isEqualTo(SENSOR_WIDTH)
assertThat(lp.height).isEqualTo(SENSOR_HEIGHT)
}
}
@Test
fun showUdfpsOverlay_withRotation180() = withRotation(Surface.ROTATION_180) {
withReason(REASON_AUTH_BP) {
controllerOverlay.show(udfpsController, overlayParams)
verify(windowManager).addView(
eq(controllerOverlay.overlayView),
layoutParamsCaptor.capture()
)
// ROTATION_180 is not supported. Sensor should stay in the top left corner.
val lp = layoutParamsCaptor.value
assertThat(lp.x).isEqualTo(0)
assertThat(lp.y).isEqualTo(0)
assertThat(lp.width).isEqualTo(SENSOR_WIDTH)
assertThat(lp.height).isEqualTo(SENSOR_HEIGHT)
}
}
@Test
fun showUdfpsOverlay_withRotation90() = withRotation(Surface.ROTATION_90) {
withReason(REASON_AUTH_BP) {
controllerOverlay.show(udfpsController, overlayParams)
verify(windowManager).addView(
eq(controllerOverlay.overlayView),
layoutParamsCaptor.capture()
)
// Sensor should be in the bottom left corner in ROTATION_90.
val lp = layoutParamsCaptor.value
assertThat(lp.x).isEqualTo(0)
assertThat(lp.y).isEqualTo(DISPLAY_WIDTH - SENSOR_WIDTH)
assertThat(lp.width).isEqualTo(SENSOR_HEIGHT)
assertThat(lp.height).isEqualTo(SENSOR_WIDTH)
}
}
@Test
fun showUdfpsOverlay_withRotation270() = withRotation(Surface.ROTATION_270) {
withReason(REASON_AUTH_BP) {
controllerOverlay.show(udfpsController, overlayParams)
verify(windowManager).addView(
eq(controllerOverlay.overlayView),
layoutParamsCaptor.capture()
)
// Sensor should be in the top right corner in ROTATION_270.
val lp = layoutParamsCaptor.value
assertThat(lp.x).isEqualTo(DISPLAY_HEIGHT - SENSOR_HEIGHT)
assertThat(lp.y).isEqualTo(0)
assertThat(lp.width).isEqualTo(SENSOR_HEIGHT)
assertThat(lp.height).isEqualTo(SENSOR_WIDTH)
}
}
private fun showUdfpsOverlay(isEnrollUseCase: Boolean = false) {
val didShow = controllerOverlay.show(udfpsController, overlayParams)
verify(windowManager).addView(eq(controllerOverlay.overlayView), any())
verify(udfpsView).setUdfpsDisplayModeProvider(eq(udfpsDisplayMode))
verify(udfpsView).animationViewController = any()
verify(udfpsView).addView(any())
assertThat(didShow).isTrue()
assertThat(controllerOverlay.isShowing).isTrue()
assertThat(controllerOverlay.isHiding).isFalse()
assertThat(controllerOverlay.overlayView).isNotNull()
if (isEnrollUseCase) {
verify(udfpsEnrollView).updateSensorLocation(eq(overlayParams.sensorBounds))
assertThat(controllerOverlay.enrollHelper).isNotNull()
} else {
assertThat(controllerOverlay.enrollHelper).isNull()
}
}
@Test
fun hideUdfpsOverlay_bp() = withReason(REASON_AUTH_BP) { hideUdfpsOverlay() }
@Test
fun hideUdfpsOverlay_keyguard() = withReason(REASON_AUTH_KEYGUARD) { hideUdfpsOverlay() }
@Test
fun hideUdfpsOverlay_settings() = withReason(REASON_AUTH_SETTINGS) { hideUdfpsOverlay() }
@Test
fun hideUdfpsOverlay_locate() = withReason(REASON_ENROLL_FIND_SENSOR) { hideUdfpsOverlay() }
@Test
fun hideUdfpsOverlay_enroll() = withReason(REASON_ENROLL_ENROLLING) { hideUdfpsOverlay() }
@Test
fun hideUdfpsOverlay_other() = withReason(REASON_AUTH_OTHER) { hideUdfpsOverlay() }
private fun hideUdfpsOverlay() {
val didShow = controllerOverlay.show(udfpsController, overlayParams)
val view = controllerOverlay.overlayView
val didHide = controllerOverlay.hide()
verify(windowManager).removeView(eq(view))
assertThat(didShow).isTrue()
assertThat(didHide).isTrue()
assertThat(controllerOverlay.overlayView).isNull()
assertThat(controllerOverlay.animationViewController).isNull()
assertThat(controllerOverlay.isShowing).isFalse()
assertThat(controllerOverlay.isHiding).isTrue()
}
@Test
fun canNotHide() = withReason(REASON_AUTH_BP) {
assertThat(controllerOverlay.hide()).isFalse()
}
@Test
fun canNotReshow() = withReason(REASON_AUTH_BP) {
assertThat(controllerOverlay.show(udfpsController, overlayParams)).isTrue()
assertThat(controllerOverlay.show(udfpsController, overlayParams)).isFalse()
}
@Test
fun forwardEnrollProgressEvents() = withReason(REASON_ENROLL_ENROLLING) {
controllerOverlay.show(udfpsController, overlayParams)
with(EnrollListener(controllerOverlay)) {
controllerOverlay.onEnrollmentProgress(/* remaining */20)
controllerOverlay.onAcquiredGood()
assertThat(progress).isTrue()
assertThat(help).isFalse()
assertThat(acquired).isFalse()
}
}
@Test
fun forwardEnrollHelpEvents() = withReason(REASON_ENROLL_ENROLLING) {
controllerOverlay.show(udfpsController, overlayParams)
with(EnrollListener(controllerOverlay)) {
controllerOverlay.onEnrollmentHelp()
assertThat(progress).isFalse()
assertThat(help).isTrue()
assertThat(acquired).isFalse()
}
}
@Test
fun forwardEnrollAcquiredEvents() = withReason(REASON_ENROLL_ENROLLING) {
controllerOverlay.show(udfpsController, overlayParams)
with(EnrollListener(controllerOverlay)) {
controllerOverlay.onEnrollmentProgress(/* remaining */ 1)
controllerOverlay.onAcquiredGood()
assertThat(progress).isTrue()
assertThat(help).isFalse()
assertThat(acquired).isTrue()
}
}
@Test
fun cancels() = withReason(REASON_AUTH_BP) {
controllerOverlay.cancel()
verify(controllerCallback).onUserCanceled()
}
@Test
fun unconfigureDisplayOnHide() = withReason(REASON_AUTH_BP) {
whenever(udfpsView.isDisplayConfigured).thenReturn(true)
controllerOverlay.show(udfpsController, overlayParams)
controllerOverlay.hide()
verify(udfpsView).unconfigureDisplay()
}
@Test
fun matchesRequestIds() = withReason(REASON_AUTH_BP) {
assertThat(controllerOverlay.matchesRequestId(REQUEST_ID)).isTrue()
assertThat(controllerOverlay.matchesRequestId(REQUEST_ID + 1)).isFalse()
}
@Test
fun testTouchOutsideAreaNoRotation() = withReason(REASON_ENROLL_ENROLLING) {
val touchHints =
context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
val rotation = Surface.ROTATION_0
// touch at 0 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, 0.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[0])
// touch at 90 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, -1.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[1])
// touch at 180 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-1.0f /* x */, 0.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[2])
// touch at 270 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, 1.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[3])
}
fun testTouchOutsideAreaNoRotation90Degrees() = withReason(REASON_ENROLL_ENROLLING) {
val touchHints =
context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
val rotation = Surface.ROTATION_90
// touch at 0 degrees -> 90 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, 0.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[1])
// touch at 90 degrees -> 180 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, -1.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[2])
// touch at 180 degrees -> 270 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-1.0f /* x */, 0.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[3])
// touch at 270 degrees -> 0 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, 1.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[0])
}
fun testTouchOutsideAreaNoRotation270Degrees() = withReason(REASON_ENROLL_ENROLLING) {
val touchHints =
context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
val rotation = Surface.ROTATION_270
// touch at 0 degrees -> 270 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, 0.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[3])
// touch at 90 degrees -> 0 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, -1.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[0])
// touch at 180 degrees -> 90 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
-1.0f /* x */, 0.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[1])
// touch at 270 degrees -> 180 degrees
assertThat(
controllerOverlay.onTouchOutsideOfSensorAreaImpl(
0.0f /* x */, 1.0f /* y */,
0.0f /* sensorX */, 0.0f /* sensorY */, rotation
)
).isEqualTo(touchHints[2])
}
}
private class EnrollListener(
overlay: UdfpsControllerOverlay,
var progress: Boolean = false,
var help: Boolean = false,
var acquired: Boolean = false
) : UdfpsEnrollHelper.Listener {
init {
overlay.enrollHelper!!.setListener(this)
}
override fun onEnrollmentProgress(remaining: Int, totalSteps: Int) {
progress = true
}
override fun onEnrollmentHelp(remaining: Int, totalSteps: Int) {
help = true
}
override fun onLastStepAcquired() {
acquired = true
}
}