1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.biometrics
18 
19 import android.graphics.PointF
20 import android.graphics.RectF
21 import android.hardware.biometrics.SensorLocationInternal
22 import androidx.test.ext.junit.runners.AndroidJUnit4
23 import android.testing.TestableLooper
24 import android.testing.ViewUtils
25 import android.view.LayoutInflater
26 import android.view.Surface
27 import androidx.test.filters.SmallTest
28 import com.android.settingslib.udfps.UdfpsOverlayParams
29 import com.android.systemui.R
30 import com.android.systemui.RoboPilotTest
31 import com.android.systemui.SysuiTestCase
32 import com.android.systemui.util.mockito.any
33 import com.android.systemui.util.mockito.mock
34 import com.android.systemui.util.mockito.withArgCaptor
35 import com.google.common.truth.Truth.assertThat
36 import org.junit.After
37 import org.junit.Before
38 import org.junit.Rule
39 import org.junit.Test
40 import org.junit.runner.RunWith
41 import org.mockito.Mock
42 import org.mockito.Mockito.never
43 import org.mockito.Mockito.nullable
44 import org.mockito.Mockito.verify
45 import org.mockito.Mockito.`when` as whenever
46 import org.mockito.junit.MockitoJUnit
47 
48 private const val SENSOR_X = 50
49 private const val SENSOR_Y = 250
50 private const val SENSOR_RADIUS = 10
51 
52 @SmallTest
53 @RoboPilotTest
54 @RunWith(AndroidJUnit4::class)
55 @TestableLooper.RunWithLooper
56 class UdfpsViewTest : SysuiTestCase() {
57 
58     @JvmField @Rule
59     var rule = MockitoJUnit.rule()
60 
61     @Mock
62     lateinit var hbmProvider: UdfpsDisplayModeProvider
63     @Mock
64     lateinit var animationViewController: UdfpsAnimationViewController<UdfpsAnimationView>
65 
66     private lateinit var view: UdfpsView
67 
68     @Before
69     fun setup() {
70         context.setTheme(R.style.Theme_AppCompat)
71         view = LayoutInflater.from(context).inflate(R.layout.udfps_view, null) as UdfpsView
72         view.animationViewController = animationViewController
73         val sensorBounds = SensorLocationInternal("", SENSOR_X, SENSOR_Y, SENSOR_RADIUS).rect
74         view.overlayParams = UdfpsOverlayParams(sensorBounds, sensorBounds, 1920,
75             1080, 1f, Surface.ROTATION_0)
76         view.setUdfpsDisplayModeProvider(hbmProvider)
77         ViewUtils.attachView(view)
78     }
79 
80     @After
81     fun cleanup() {
82         ViewUtils.detachView(view)
83     }
84 
85     @Test
86     fun layoutSizeFitsSensor() {
87         val params = withArgCaptor<RectF> {
88             verify(animationViewController).onSensorRectUpdated(capture())
89         }
90         assertThat(params.width()).isAtLeast(2f * SENSOR_RADIUS)
91         assertThat(params.height()).isAtLeast(2f * SENSOR_RADIUS)
92     }
93 
94     @Test
95     fun isWithinSensorAreaAndPaused() = isWithinSensorArea(paused = true)
96 
97     @Test
98     fun isWithinSensorAreaAndNotPaused() = isWithinSensorArea(paused = false)
99 
100     private fun isWithinSensorArea(paused: Boolean) {
101         whenever(animationViewController.shouldPauseAuth()).thenReturn(paused)
102         whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f))
103         val end = (SENSOR_RADIUS * 2) - 1
104         for (x in 1 until end) {
105             for (y in 1 until end) {
106                 assertThat(view.isWithinSensorArea(x.toFloat(), y.toFloat())).isEqualTo(!paused)
107             }
108         }
109     }
110 
111     @Test
112     fun isWithinSensorAreaWhenTranslated() {
113         val offset = PointF(100f, 200f)
114         whenever(animationViewController.touchTranslation).thenReturn(offset)
115         val end = (SENSOR_RADIUS * 2) - 1
116         for (x in 0 until offset.x.toInt() step 2) {
117             for (y in 0 until offset.y.toInt() step 2) {
118                 assertThat(view.isWithinSensorArea(x.toFloat(), y.toFloat())).isFalse()
119             }
120         }
121         for (x in offset.x.toInt() + 1 until offset.x.toInt() + end) {
122             for (y in offset.y.toInt() + 1 until offset.y.toInt() + end) {
123                 assertThat(view.isWithinSensorArea(x.toFloat(), y.toFloat())).isTrue()
124             }
125         }
126     }
127 
128     @Test
129     fun isNotWithinSensorArea() {
130         whenever(animationViewController.touchTranslation).thenReturn(PointF(0f, 0f))
131         assertThat(view.isWithinSensorArea(SENSOR_RADIUS * 2.5f, SENSOR_RADIUS.toFloat()))
132             .isFalse()
133         assertThat(view.isWithinSensorArea(SENSOR_RADIUS.toFloat(), SENSOR_RADIUS * 2.5f)).isFalse()
134     }
135 
136     @Test
137     fun startAndStopIllumination() {
138         val onDone: Runnable = mock()
139         view.configureDisplay(onDone)
140 
141         val illuminator = withArgCaptor<Runnable> {
142             verify(hbmProvider).enable(capture())
143         }
144 
145         assertThat(view.isDisplayConfigured).isTrue()
146         verify(animationViewController).onDisplayConfiguring()
147         verify(animationViewController, never()).onDisplayUnconfigured()
148         verify(onDone, never()).run()
149 
150         // fake illumination event
151         illuminator.run()
152         waitForLooper()
153         verify(onDone).run()
154         verify(hbmProvider, never()).disable(any())
155 
156         view.unconfigureDisplay()
157         assertThat(view.isDisplayConfigured).isFalse()
158         verify(animationViewController).onDisplayUnconfigured()
159         verify(hbmProvider).disable(nullable(Runnable::class.java))
160     }
161 
162     private fun waitForLooper() = TestableLooper.get(this).processAllMessages()
163 }
164