blob: 4a5b23c02e4097fac9830c2f198f4e4068ae6745 [file] [log] [blame]
/*
* Copyright (C) 2022 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.app.admin.DevicePolicyManager
import android.hardware.biometrics.BiometricAuthenticator
import android.hardware.biometrics.BiometricConstants
import android.hardware.biometrics.BiometricManager
import android.hardware.biometrics.PromptInfo
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.os.Handler
import android.os.IBinder
import android.os.UserManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.testing.ViewUtils
import android.view.KeyEvent
import android.view.View
import android.view.WindowInsets
import android.view.WindowManager
import android.widget.ScrollView
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
class AuthContainerViewTest : SysuiTestCase() {
@JvmField @Rule
var mockitoRule = MockitoJUnit.rule()
@Mock
lateinit var callback: AuthDialogCallback
@Mock
lateinit var userManager: UserManager
@Mock
lateinit var lockPatternUtils: LockPatternUtils
@Mock
lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Mock
lateinit var windowToken: IBinder
@Mock
lateinit var interactionJankMonitor: InteractionJankMonitor
private var authContainer: TestAuthContainerView? = null
@After
fun tearDown() {
if (authContainer?.isAttachedToWindow == true) {
ViewUtils.detachView(authContainer)
}
}
@Test
fun testNotifiesAnimatedIn() {
initializeFingerprintContainer()
verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
}
@Test
fun testDismissesOnBack() {
val container = initializeFingerprintContainer(addToView = true)
assertThat(container.parent).isNotNull()
val root = container.rootView
// Simulate back invocation
container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK))
container.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK))
waitForIdleSync()
assertThat(container.parent).isNull()
assertThat(root.isAttachedToWindow).isFalse()
}
@Test
fun testIgnoresAnimatedInWhenDismissed() {
val container = initializeFingerprintContainer(addToView = false)
container.dismissFromSystemServer()
waitForIdleSync()
verify(callback, never()).onDialogAnimatedIn(anyLong())
container.addToView()
waitForIdleSync()
// attaching the view resets the state and allows this to happen again
verify(callback).onDialogAnimatedIn(authContainer?.requestId ?: 0L)
}
@Test
fun testDismissesOnFocusLoss() {
val container = initializeFingerprintContainer()
waitForIdleSync()
val requestID = authContainer?.requestId ?: 0L
verify(callback).onDialogAnimatedIn(requestID)
container.onWindowFocusChanged(false)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
eq<ByteArray?>(null), /* credentialAttestation */
eq(requestID)
)
assertThat(container.parent).isNull()
}
@Test
fun testActionAuthenticated_sendsDismissedAuthenticated() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_AUTHENTICATED
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BIOMETRIC_AUTHENTICATED),
eq<ByteArray?>(null), /* credentialAttestation */
eq(authContainer?.requestId ?: 0L)
)
assertThat(container.parent).isNull()
}
@Test
fun testActionUserCanceled_sendsDismissedUserCanceled() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USER_CANCELED
)
waitForIdleSync()
verify(callback).onSystemEvent(
eq(BiometricConstants.BIOMETRIC_SYSTEM_EVENT_EARLY_USER_CANCEL),
eq(authContainer?.requestId ?: 0L)
)
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
eq<ByteArray?>(null), /* credentialAttestation */
eq(authContainer?.requestId ?: 0L)
)
assertThat(container.parent).isNull()
}
@Test
fun testActionButtonNegative_sendsDismissedButtonNegative() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_NEGATIVE
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_BUTTON_NEGATIVE),
eq<ByteArray?>(null), /* credentialAttestation */
eq(authContainer?.requestId ?: 0L)
)
assertThat(container.parent).isNull()
}
@Test
fun testActionTryAgain_sendsTryAgain() {
val container = initializeFingerprintContainer(
authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK
)
container.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_BUTTON_TRY_AGAIN
)
waitForIdleSync()
verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L)
}
@Test
fun testActionError_sendsDismissedError() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_ERROR
)
waitForIdleSync()
verify(callback).onDismissed(
eq(AuthDialogCallback.DISMISSED_ERROR),
eq<ByteArray?>(null), /* credentialAttestation */
eq(authContainer?.requestId ?: 0L)
)
assertThat(authContainer!!.parent).isNull()
}
@Test
fun testActionUseDeviceCredential_sendsOnDeviceCredentialPressed() {
val container = initializeFingerprintContainer(
authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
container.mBiometricCallback.onAction(
AuthBiometricView.Callback.ACTION_USE_DEVICE_CREDENTIAL
)
waitForIdleSync()
verify(callback).onDeviceCredentialPressed(authContainer?.requestId ?: 0L)
assertThat(container.hasCredentialView()).isTrue()
}
@Test
fun testAnimateToCredentialUI_invokesStartTransitionToCredentialUI() {
val container = initializeFingerprintContainer(
authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or
BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
container.animateToCredentialUI()
waitForIdleSync()
assertThat(container.hasCredentialView()).isTrue()
}
@Test
fun testShowBiometricUI() {
val container = initializeFingerprintContainer()
waitForIdleSync()
assertThat(container.hasCredentialView()).isFalse()
assertThat(container.hasBiometricPrompt()).isTrue()
}
@Test
fun testShowCredentialUI() {
val container = initializeFingerprintContainer(
authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
waitForIdleSync()
assertThat(container.hasCredentialView()).isTrue()
assertThat(container.hasBiometricPrompt()).isFalse()
}
@Test
fun testCredentialViewUsesEffectiveUserId() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(200)
whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(200))).thenReturn(
DevicePolicyManager.PASSWORD_QUALITY_SOMETHING
)
val container = initializeFingerprintContainer(
authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
waitForIdleSync()
assertThat(container.hasCredentialPatternView()).isTrue()
assertThat(container.hasBiometricPrompt()).isFalse()
}
@Test
fun testCredentialUI_disablesClickingOnBackground() {
whenever(userManager.getCredentialOwnerProfile(anyInt())).thenReturn(20)
whenever(lockPatternUtils.getKeyguardStoredPasswordQuality(eq(20))).thenReturn(
DevicePolicyManager.PASSWORD_QUALITY_NUMERIC
)
// In the credential view, clicking on the background (to cancel authentication) is not
// valid. Thus, the listener should be null, and it should not be in the accessibility
// hierarchy.
val container = initializeFingerprintContainer(
authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
waitForIdleSync()
assertThat(container.hasCredentialPasswordView()).isTrue()
assertThat(container.hasBiometricPrompt()).isFalse()
assertThat(
container.findViewById<View>(R.id.background)?.isImportantForAccessibility
).isFalse()
container.findViewById<View>(R.id.background)?.performClick()
waitForIdleSync()
assertThat(container.hasCredentialPasswordView()).isTrue()
assertThat(container.hasBiometricPrompt()).isFalse()
}
@Test
fun testLayoutParams_hasSecureWindowFlag() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
assertThat((layoutParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0).isTrue()
}
@Test
fun testLayoutParams_hasDimbehindWindowFlag() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
val lpFlags = layoutParams.flags
val lpDimAmount = layoutParams.dimAmount
assertThat((lpFlags and WindowManager.LayoutParams.FLAG_DIM_BEHIND) != 0).isTrue()
assertThat(lpDimAmount).isGreaterThan(0f)
}
@Test
fun testLayoutParams_excludesImeInsets() {
val layoutParams = AuthContainerView.getLayoutParams(windowToken, "")
assertThat((layoutParams.fitInsetsTypes and WindowInsets.Type.ime()) == 0).isTrue()
}
@Test
fun coexFaceRestartsOnTouch() {
val container = initializeCoexContainer()
container.onPointerDown()
waitForIdleSync()
container.onAuthenticationFailed(BiometricAuthenticator.TYPE_FACE, "failed")
waitForIdleSync()
verify(callback, never()).onTryAgainPressed(anyLong())
container.onPointerDown()
waitForIdleSync()
verify(callback).onTryAgainPressed(authContainer?.requestId ?: 0L)
}
private fun initializeFingerprintContainer(
authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
addToView: Boolean = true
) = initializeContainer(
TestAuthContainerView(
authenticators = authenticators,
fingerprintProps = fingerprintSensorPropertiesInternal()
),
addToView
)
private fun initializeCoexContainer(
authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
addToView: Boolean = true
) = initializeContainer(
TestAuthContainerView(
authenticators = authenticators,
fingerprintProps = fingerprintSensorPropertiesInternal(),
faceProps = faceSensorPropertiesInternal()
),
addToView
)
private fun initializeContainer(
view: TestAuthContainerView,
addToView: Boolean
): TestAuthContainerView {
authContainer = view
if (addToView) {
authContainer!!.addToView()
}
return authContainer!!
}
private inner class TestAuthContainerView(
authenticators: Int = BiometricManager.Authenticators.BIOMETRIC_WEAK,
fingerprintProps: List<FingerprintSensorPropertiesInternal> = listOf(),
faceProps: List<FaceSensorPropertiesInternal> = listOf()
) : AuthContainerView(
Config().apply {
mContext = this@AuthContainerViewTest.context
mCallback = callback
mSensorIds = (fingerprintProps.map { it.sensorId } +
faceProps.map { it.sensorId }).toIntArray()
mSkipAnimation = true
mPromptInfo = PromptInfo().apply {
this.authenticators = authenticators
}
},
fingerprintProps,
faceProps,
wakefulnessLifecycle,
userManager,
lockPatternUtils,
interactionJankMonitor,
Handler(TestableLooper.get(this).looper),
FakeExecutor(FakeSystemClock())
) {
override fun postOnAnimation(runnable: Runnable) {
runnable.run()
}
}
override fun waitForIdleSync() = TestableLooper.get(this).processAllMessages()
private fun AuthContainerView.addToView() {
ViewUtils.attachView(this)
waitForIdleSync()
assertThat(isAttachedToWindow).isTrue()
}
}
private fun AuthContainerView.hasBiometricPrompt() =
(findViewById<ScrollView>(R.id.biometric_scrollview)?.childCount ?: 0) > 0
private fun AuthContainerView.hasCredentialView() =
hasCredentialPatternView() || hasCredentialPasswordView()
private fun AuthContainerView.hasCredentialPatternView() =
findViewById<View>(R.id.lockPattern) != null
private fun AuthContainerView.hasCredentialPasswordView() =
findViewById<View>(R.id.lockPassword) != null