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.TestableLooper
20 import android.view.View
21 import android.view.ViewGroup
22 import androidx.constraintlayout.widget.ConstraintLayout
23 import androidx.constraintlayout.widget.ConstraintSet
24 import androidx.test.ext.junit.runners.AndroidJUnit4
25 import androidx.test.filters.SmallTest
26 import com.android.internal.logging.UiEventLogger
27 import com.android.internal.util.LatencyTracker
28 import com.android.internal.widget.LockPatternUtils
29 import com.android.internal.widget.LockscreenCredential
30 import com.android.keyguard.KeyguardPinViewController.PinBouncerUiEvent
31 import com.android.keyguard.KeyguardSecurityModel.SecurityMode
32 import com.android.keyguard.domain.interactor.KeyguardKeyboardInteractor
33 import com.android.systemui.SysuiTestCase
34 import com.android.systemui.classifier.FalsingCollector
35 import com.android.systemui.classifier.FalsingCollectorFake
36 import com.android.systemui.flags.FeatureFlags
37 import com.android.systemui.flags.Flags
38 import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository
39 import com.android.systemui.res.R
40 import com.android.systemui.statusbar.policy.DevicePostureController
41 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED
42 import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED
43 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
44 import com.android.systemui.util.mockito.whenever
45 import com.google.common.truth.Truth.assertThat
46 import org.junit.Before
47 import org.junit.Test
48 import org.junit.runner.RunWith
49 import org.mockito.ArgumentCaptor
50 import org.mockito.ArgumentMatchers.anyBoolean
51 import org.mockito.ArgumentMatchers.anyInt
52 import org.mockito.ArgumentMatchers.anyString
53 import org.mockito.Captor
54 import org.mockito.Mock
55 import org.mockito.Mockito
56 import org.mockito.Mockito.any
57 import org.mockito.Mockito.verify
58 import org.mockito.Mockito.`when`
59 import org.mockito.MockitoAnnotations
60 
61 @SmallTest
62 @RunWith(AndroidJUnit4::class)
63 // collectFlow in KeyguardPinBasedInputViewController.onViewAttached calls JavaAdapter.CollectFlow,
64 // which calls View.onRepeatWhenAttached, which requires being run on main thread.
65 @TestableLooper.RunWithLooper(setAsMainLooper = true)
66 class KeyguardPinViewControllerTest : SysuiTestCase() {
67 
68     private lateinit var objectKeyguardPINView: KeyguardPINView
69 
70     @Mock private lateinit var mockKeyguardPinView: KeyguardPINView
71 
72     @Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea
73 
74     @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
75 
76     @Mock private lateinit var securityMode: SecurityMode
77 
78     @Mock private lateinit var lockPatternUtils: LockPatternUtils
79 
80     @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback
81 
82     @Mock
83     private lateinit var keyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory
84 
85     @Mock
86     private lateinit var keyguardMessageAreaController:
87         KeyguardMessageAreaController<BouncerKeyguardMessageArea>
88 
89     @Mock private lateinit var mLatencyTracker: LatencyTracker
90 
91     @Mock private lateinit var liftToActivateListener: LiftToActivateListener
92 
93     @Mock private val mEmergencyButtonController: EmergencyButtonController? = null
94     private val falsingCollector: FalsingCollector = FalsingCollectorFake()
95     private val keyguardKeyboardInteractor = KeyguardKeyboardInteractor(FakeKeyboardRepository())
96     private val passwordTextViewLayoutParams =
97         ViewGroup.LayoutParams(/* width= */ 0, /* height= */ 0)
98     @Mock lateinit var postureController: DevicePostureController
99     @Mock lateinit var mSelectedUserInteractor: SelectedUserInteractor
100 
101     @Mock lateinit var featureFlags: FeatureFlags
102     @Mock lateinit var passwordTextView: PasswordTextView
103     @Mock lateinit var deleteButton: NumPadButton
104     @Mock lateinit var enterButton: View
105     @Mock lateinit var uiEventLogger: UiEventLogger
106 
107     @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback>
108 
109     @Before
setupnull110     fun setup() {
111         MockitoAnnotations.initMocks(this)
112         `when`(mockKeyguardPinView.requireViewById<View>(R.id.bouncer_message_area))
113             .thenReturn(keyguardMessageArea)
114         `when`(keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java)))
115             .thenReturn(keyguardMessageAreaController)
116         `when`(mockKeyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry)
117         `when`(mockKeyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry))
118             .thenReturn(passwordTextView)
119         `when`(mockKeyguardPinView.resources).thenReturn(context.resources)
120         `when`(mockKeyguardPinView.findViewById<NumPadButton>(R.id.delete_button))
121             .thenReturn(deleteButton)
122         `when`(mockKeyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton)
123         // For posture tests:
124         `when`(mockKeyguardPinView.buttons).thenReturn(arrayOf())
125         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
126         `when`(featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)).thenReturn(false)
127         `when`(passwordTextView.layoutParams).thenReturn(passwordTextViewLayoutParams)
128 
129         objectKeyguardPINView =
130             View.inflate(mContext, R.layout.keyguard_pin_view, null)
131                 .requireViewById(R.id.keyguard_pin_view) as KeyguardPINView
132     }
133 
constructPinViewControllernull134     private fun constructPinViewController(
135         mKeyguardPinView: KeyguardPINView
136     ): KeyguardPinViewController {
137         return KeyguardPinViewController(
138             mKeyguardPinView,
139             keyguardUpdateMonitor,
140             securityMode,
141             lockPatternUtils,
142             mKeyguardSecurityCallback,
143             keyguardMessageAreaControllerFactory,
144             mLatencyTracker,
145             liftToActivateListener,
146             mEmergencyButtonController,
147             falsingCollector,
148             postureController,
149             featureFlags,
150             mSelectedUserInteractor,
151             uiEventLogger,
152             keyguardKeyboardInteractor
153         )
154     }
155 
156     @Test
onViewAttached_deviceHalfFolded_propagatedToPatternViewnull157     fun onViewAttached_deviceHalfFolded_propagatedToPatternView() {
158         val pinViewController = constructPinViewController(objectKeyguardPINView)
159         overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
160         whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
161 
162         pinViewController.onViewAttached()
163 
164         assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
165     }
166 
167     @Test
onDevicePostureChanged_deviceOpened_propagatedToPatternViewnull168     fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() {
169         val pinViewController = constructPinViewController(objectKeyguardPINView)
170         overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f)
171 
172         whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED)
173         pinViewController.onViewAttached()
174 
175         // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED
176         assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio())
177 
178         // Simulate posture change to state DEVICE_POSTURE_OPENED with callback
179         verify(postureController).addCallback(postureCallbackCaptor.capture())
180         val postureCallback: DevicePostureController.Callback = postureCallbackCaptor.value
181         postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
182 
183         // Verify view is now in posture state DEVICE_POSTURE_OPENED
184         assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
185 
186         // Simulate posture change to same state with callback
187         postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED)
188 
189         // Verify view is still in posture state DEVICE_POSTURE_OPENED
190         assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio())
191     }
192 
getPinTopGuidelinenull193     private fun getPinTopGuideline(): Float {
194         val cs = ConstraintSet()
195         val container =
196             objectKeyguardPINView.requireViewById(R.id.pin_container) as ConstraintLayout
197         cs.clone(container)
198         return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent
199     }
200 
getHalfOpenedBouncerHeightRationull201     private fun getHalfOpenedBouncerHeightRatio(): Float {
202         return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio)
203     }
204 
205     @Test
testOnViewAttachednull206     fun testOnViewAttached() {
207         val pinViewController = constructPinViewController(mockKeyguardPinView)
208 
209         pinViewController.onViewAttached()
210 
211         verify(keyguardMessageAreaController)
212             .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false)
213     }
214 
215     @Test
testOnViewAttached_withExistingMessagenull216     fun testOnViewAttached_withExistingMessage() {
217         val pinViewController = constructPinViewController(mockKeyguardPinView)
218         Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.")
219 
220         pinViewController.onViewAttached()
221 
222         verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean())
223     }
224 
225     @Test
testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5null226     fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() {
227         val pinViewController = constructPinViewController(mockKeyguardPinView)
228         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
229         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
230         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
231         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3)
232         `when`(passwordTextView.text).thenReturn("")
233 
234         pinViewController.onViewAttached()
235 
236         verify(deleteButton).visibility = View.INVISIBLE
237         verify(enterButton).visibility = View.INVISIBLE
238         verify(passwordTextView).setUsePinShapes(true)
239         verify(passwordTextView).setIsPinHinting(true)
240     }
241 
242     @Test
testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5null243     fun testOnViewAttached_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() {
244         val pinViewController = constructPinViewController(mockKeyguardPinView)
245         `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
246         `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
247         `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
248         `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6)
249         `when`(passwordTextView.text).thenReturn("")
250 
251         pinViewController.onViewAttached()
252 
253         verify(deleteButton).visibility = View.VISIBLE
254         verify(enterButton).visibility = View.VISIBLE
255         verify(passwordTextView).setUsePinShapes(true)
256         verify(passwordTextView).setIsPinHinting(false)
257     }
258 
259     @Test
handleLockout_readsNumberOfErrorAttemptsnull260     fun handleLockout_readsNumberOfErrorAttempts() {
261         val pinViewController = constructPinViewController(mockKeyguardPinView)
262 
263         pinViewController.handleAttemptLockout(0)
264 
265         verify(lockPatternUtils).getCurrentFailedPasswordAttempts(anyInt())
266     }
267 
268     @Test
onUserInput_autoConfirmation_attemptsUnlocknull269     fun onUserInput_autoConfirmation_attemptsUnlock() {
270         val pinViewController = constructPinViewController(mockKeyguardPinView)
271         whenever(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true)
272         whenever(lockPatternUtils.getPinLength(anyInt())).thenReturn(6)
273         whenever(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true)
274         whenever(passwordTextView.text).thenReturn("000000")
275         whenever(enterButton.visibility).thenReturn(View.INVISIBLE)
276         whenever(mockKeyguardPinView.enteredCredential)
277             .thenReturn(LockscreenCredential.createPin("000000"))
278 
279         pinViewController.onUserInput()
280 
281         verify(uiEventLogger).log(PinBouncerUiEvent.ATTEMPT_UNLOCK_WITH_AUTO_CONFIRM_FEATURE)
282         verify(keyguardUpdateMonitor).setCredentialAttempted()
283     }
284 }
285