1 /*
2  * Copyright (C) 2022 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.keyguard
18 
19 import android.testing.AndroidTestingRunner
20 import android.testing.TestableLooper
21 import android.view.View
22 import androidx.constraintlayout.widget.ConstraintLayout
23 import androidx.constraintlayout.widget.ConstraintSet
24 import androidx.test.filters.SmallTest
25 import com.android.internal.util.LatencyTracker
26 import com.android.internal.widget.LockPatternUtils
27 import com.android.keyguard.KeyguardSecurityModel.SecurityMode
28 import com.android.systemui.R
29 import com.android.systemui.SysuiTestCase
30 import com.android.systemui.classifier.FalsingCollector
31 import com.android.systemui.classifier.FalsingCollectorFake
32 import com.android.systemui.flags.FeatureFlags
33 import com.android.systemui.flags.Flags
34 import com.android.systemui.statusbar.policy.DevicePostureController
35 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
36 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
37 import com.android.systemui.util.mockito.whenever
38 import com.google.common.truth.Truth.assertThat
39 import org.junit.Before
40 import org.junit.Test
41 import org.junit.runner.RunWith
42 import org.mockito.ArgumentCaptor
43 import org.mockito.ArgumentMatchers.anyBoolean
44 import org.mockito.ArgumentMatchers.anyInt
45 import org.mockito.ArgumentMatchers.anyString
46 import org.mockito.Captor
47 import org.mockito.Mock
48 import org.mockito.Mockito
49 import org.mockito.Mockito.any
50 import org.mockito.Mockito.verify
51 import org.mockito.Mockito.`when`
52 import org.mockito.MockitoAnnotations
53 
54 @SmallTest
55 @RunWith(AndroidTestingRunner::class)
56 @TestableLooper.RunWithLooper
57 class KeyguardPinViewControllerTest : SysuiTestCase() {
58 
59     private lateinit var objectKeyguardPINView: KeyguardPINView
60 
61     @Mock private lateinit var mockKeyguardPinView: KeyguardPINView
62 
63     @Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea
64 
65     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
66 
67     @Mock private lateinit var securityMode: SecurityMode
68 
69     @Mock private lateinit var lockPatternUtils: LockPatternUtils
70 
71     @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
72 
73     @Mock
74     private lateinit var keyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
75 
76     @Mock
77     private lateinit var keyguardMessageAreaController:
78         KeyguardMessageAreaController<BouncerKeyguardMessageArea>
79 
80     @Mock private lateinit var mLatencyTracker: LatencyTracker
81 
82     @Mock private lateinit var liftToActivateListener: LiftToActivateListener
83 
84     @Mock private val mEmergencyButtonController: EmergencyButtonController? = null
85     private val falsingCollector: FalsingCollector = FalsingCollectorFake()
86     @Mock lateinit var postureController: DevicePostureController
87 
88     @Mock lateinit var featureFlags: FeatureFlags
89     @Mock lateinit var passwordTextView: PasswordTextView
90     @Mock lateinit var deleteButton: NumPadButton
91     @Mock lateinit var enterButton: View
92 
93     @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
94 
95     @Before
96     fun setup() {
97         MockitoAnnotations.initMocks(this)
98         Mockito.`when`(mockKeyguardPinView.requireViewById<View>(R.id.bouncer_message_area))
99             .thenReturn(keyguardMessageArea)
100         Mockito.`when`(
101                 keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java))
102             )
103             .thenReturn(keyguardMessageAreaController)
104         `when`(mockKeyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry)
105         `when`(mockKeyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry))
106             .thenReturn(passwordTextView)
107         `when`(mockKeyguardPinView.resources).thenReturn(context.resources)
108         `when`(mockKeyguardPinView.findViewById<NumPadButton>(R.id.delete_button))
109             .thenReturn(deleteButton)
110         `when`(mockKeyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton)
111         // For posture tests:
112         `when`(mockKeyguardPinView.buttons).thenReturn(arrayOf())
113         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
114 
115         objectKeyguardPINView =
116             View.inflate(mContext, R.layout.keyguard_pin_view, null)
117                 .requireViewById(R.id.keyguard_pin_view) as KeyguardPINView
118     }
119 
120     private fun constructPinViewController(
121         mKeyguardPinView: KeyguardPINView
122     ): KeyguardPinViewController {
123         return KeyguardPinViewController(
124             mKeyguardPinView,
125             keyguardUpdateMonitor,
126             securityMode,
127             lockPatternUtils,
128             mKeyguardSecurityCallback,
129             keyguardMessageAreaControllerFactory,
130             mLatencyTracker,
131             liftToActivateListener,
132             mEmergencyButtonController,
133             falsingCollector,
134             postureController,
135             featureFlags
136         )
137     }
138 
139     @Test
140     fun onViewAttached_deviceHalfFolded_propagatedToPatternView() {
141         val pinViewController = constructPinViewController(objectKeyguardPINView)
142         overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
143         whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
144 
145         pinViewController.onViewAttached()
146 
147         assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
148     }
149 
150     @Test
151     fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() {
152         val pinViewController = constructPinViewController(objectKeyguardPINView)
153         overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
154 
155         whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
156         pinViewController.onViewAttached()
157 
158         // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
159         assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
160 
161         // Simulate posture change to state DEVICE_POSTURE_OPENED with callback
162         verify(postureController).addCallback(postureCallbackCaptor.capture())
163         val postureCallback: DevicePostureController.Callback = postureCallbackCaptor.value
164         postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
165 
166         // Verify view is now in posture state DEVICE_POSTURE_OPENED
167         assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
168 
169         // Simulate posture change to same state with callback
170         postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
171 
172         // Verify view is still in posture state DEVICE_POSTURE_OPENED
173         assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
174     }
175 
176     private fun getPinTopGuideline(): Float {
177         val cs = ConstraintSet()
178         val container = objectKeyguardPINView.requireViewById(R.id.pin_container) as ConstraintLayout
179         cs.clone(container)
180         return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent
181     }
182 
183     private fun getHalfOpenedBouncerHeightRatio(): Float {
184         return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
185     }
186 
187     @Test
188     fun testOnViewAttached() {
189         val pinViewController = constructPinViewController(mockKeyguardPinView)
190 
191         pinViewController.onViewAttached()
192 
193         verify(keyguardMessageAreaController)
194             .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
195     }
196 
197     @Test
198     fun testOnViewAttached_withExistingMessage() {
199         val pinViewController = constructPinViewController(mockKeyguardPinView)
200         Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
201 
202         pinViewController.onViewAttached()
203 
204         verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
205     }
206 
207     @Test
208     fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
209         val pinViewController = constructPinViewController(mockKeyguardPinView)
210         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
211         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
212         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
213         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3)
214         `when`(passwordTextView.text).thenReturn("")
215 
216         pinViewController.onViewAttached()
217 
218         verify(deleteButton).visibility = View.INVISIBLE
219         verify(enterButton).visibility = View.INVISIBLE
220         verify(passwordTextView).setUsePinShapes(true)
221         verify(passwordTextView).setIsPinHinting(true)
222     }
223 
224     @Test
225     fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
226         val pinViewController = constructPinViewController(mockKeyguardPinView)
227         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
228         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
229         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
230         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6)
231         `when`(passwordTextView.text).thenReturn("")
232 
233         pinViewController.onViewAttached()
234 
235         verify(deleteButton).visibility = View.VISIBLE
236         verify(enterButton).visibility = View.VISIBLE
237         verify(passwordTextView).setUsePinShapes(true)
238         verify(passwordTextView).setIsPinHinting(false)
239     }
240 
241     @Test
242     fun handleLockout_readsNumberOfErrorAttempts() {
243         val pinViewController = constructPinViewController(mockKeyguardPinView)
244 
245         pinViewController.handleAttemptLockout(0)
246 
247         verify(lockPatternUtils).getCurrentFailedPasswordAttempts(anyInt())
248     }
249 }
250