1 /*
2  * Copyright (C) 2020 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 static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertTrue;
21 import static org.mockito.ArgumentMatchers.anyBoolean;
22 import static org.mockito.ArgumentMatchers.anyInt;
23 import static org.mockito.Mockito.atLeast;
24 import static org.mockito.Mockito.eq;
25 import static org.mockito.Mockito.never;
26 import static org.mockito.Mockito.reset;
27 import static org.mockito.Mockito.times;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when;
30 
31 import android.content.Context;
32 import android.testing.AndroidTestingRunner;
33 import android.testing.TestableLooper.RunWithLooper;
34 
35 import androidx.test.filters.SmallTest;
36 
37 import com.android.keyguard.KeyguardUpdateMonitor;
38 import com.android.systemui.SysuiTestCase;
39 import com.android.systemui.dump.DumpManager;
40 import com.android.systemui.keyguard.KeyguardViewMediator;
41 import com.android.systemui.plugins.statusbar.StatusBarStateController;
42 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
43 import com.android.systemui.statusbar.StatusBarState;
44 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
45 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
46 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
47 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionListener;
48 import com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManager;
49 import com.android.systemui.statusbar.policy.ConfigurationController;
50 import com.android.systemui.statusbar.policy.KeyguardStateController;
51 import com.android.systemui.util.concurrency.DelayableExecutor;
52 import com.android.systemui.util.time.FakeSystemClock;
53 
54 import org.junit.Before;
55 import org.junit.Test;
56 import org.junit.runner.RunWith;
57 import org.mockito.ArgumentCaptor;
58 import org.mockito.Captor;
59 import org.mockito.Mock;
60 import org.mockito.MockitoAnnotations;
61 
62 import java.util.List;
63 
64 @SmallTest
65 @RunWith(AndroidTestingRunner.class)
66 @RunWithLooper
67 public class UdfpsKeyguardViewControllerTest extends SysuiTestCase {
68     // Dependencies
69     @Mock
70     private UdfpsKeyguardView mView;
71     @Mock
72     private Context mResourceContext;
73     @Mock
74     private StatusBarStateController mStatusBarStateController;
75     @Mock
76     private PanelExpansionStateManager mPanelExpansionStateManager;
77     @Mock
78     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
79     @Mock
80     private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
81     @Mock
82     private DumpManager mDumpManager;
83     @Mock
84     private DelayableExecutor mExecutor;
85     @Mock
86     private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
87     @Mock
88     private KeyguardStateController mKeyguardStateController;
89     @Mock
90     private KeyguardViewMediator mKeyguardViewMediator;
91     @Mock
92     private ConfigurationController mConfigurationController;
93     @Mock
94     private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
95     @Mock
96     private SystemUIDialogManager mDialogManager;
97     @Mock
98     private UdfpsController mUdfpsController;
99     private FakeSystemClock mSystemClock = new FakeSystemClock();
100 
101     private UdfpsKeyguardViewController mController;
102 
103     // Capture listeners so that they can be used to send events
104     @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
105     private StatusBarStateController.StateListener mStatusBarStateListener;
106 
107     @Captor private ArgumentCaptor<PanelExpansionListener> mExpansionListenerCaptor;
108     private List<PanelExpansionListener> mExpansionListeners;
109 
110     @Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor>
111             mAltAuthInterceptorCaptor;
112     private StatusBarKeyguardViewManager.AlternateAuthInterceptor mAltAuthInterceptor;
113 
114     @Captor private ArgumentCaptor<KeyguardStateController.Callback>
115             mKeyguardStateControllerCallbackCaptor;
116     private KeyguardStateController.Callback mKeyguardStateControllerCallback;
117 
118     @Before
setUp()119     public void setUp() {
120         MockitoAnnotations.initMocks(this);
121         when(mView.getContext()).thenReturn(mResourceContext);
122         when(mResourceContext.getString(anyInt())).thenReturn("test string");
123         when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false);
124         mController = new UdfpsKeyguardViewController(
125                 mView,
126                 mStatusBarStateController,
127                 mPanelExpansionStateManager,
128                 mStatusBarKeyguardViewManager,
129                 mKeyguardUpdateMonitor,
130                 mDumpManager,
131                 mLockscreenShadeTransitionController,
132                 mConfigurationController,
133                 mSystemClock,
134                 mKeyguardStateController,
135                 mUnlockedScreenOffAnimationController,
136                 mDialogManager,
137                 mUdfpsController);
138     }
139 
140     @Test
testRegistersExpansionChangedListenerOnAttached()141     public void testRegistersExpansionChangedListenerOnAttached() {
142         mController.onViewAttached();
143         captureExpansionListeners();
144     }
145 
146     @Test
testRegistersStatusBarStateListenersOnAttached()147     public void testRegistersStatusBarStateListenersOnAttached() {
148         mController.onViewAttached();
149         captureStatusBarStateListeners();
150     }
151 
152     @Test
testViewControllerQueriesSBStateOnAttached()153     public void testViewControllerQueriesSBStateOnAttached() {
154         mController.onViewAttached();
155         verify(mStatusBarStateController).getState();
156         verify(mStatusBarStateController).getDozeAmount();
157 
158         final float dozeAmount = .88f;
159         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE_LOCKED);
160         when(mStatusBarStateController.getDozeAmount()).thenReturn(dozeAmount);
161         captureStatusBarStateListeners();
162 
163         mController.onViewAttached();
164         verify(mView, atLeast(1)).setPauseAuth(true);
165         verify(mView).onDozeAmountChanged(dozeAmount, dozeAmount);
166     }
167 
168     @Test
testListenersUnregisteredOnDetached()169     public void testListenersUnregisteredOnDetached() {
170         mController.onViewAttached();
171         captureStatusBarStateListeners();
172         captureExpansionListeners();
173         captureKeyguardStateControllerCallback();
174         mController.onViewDetached();
175 
176         verify(mStatusBarStateController).removeCallback(mStatusBarStateListener);
177         for (PanelExpansionListener listener : mExpansionListeners) {
178             verify(mPanelExpansionStateManager).removeExpansionListener(listener);
179         }
180         verify(mKeyguardStateController).removeCallback(mKeyguardStateControllerCallback);
181     }
182 
183     @Test
testDozeEventsSentToView()184     public void testDozeEventsSentToView() {
185         mController.onViewAttached();
186         captureStatusBarStateListeners();
187 
188         final float linear = .55f;
189         final float eased = .65f;
190         mStatusBarStateListener.onDozeAmountChanged(linear, eased);
191 
192         verify(mView).onDozeAmountChanged(linear, eased);
193     }
194 
195     @Test
testShouldPauseAuthBouncerShowing()196     public void testShouldPauseAuthBouncerShowing() {
197         mController.onViewAttached();
198         captureStatusBarStateListeners();
199 
200         sendStatusBarStateChanged(StatusBarState.KEYGUARD);
201 
202         assertFalse(mController.shouldPauseAuth());
203     }
204 
205     @Test
testShouldNotPauseAuthOnKeyguard()206     public void testShouldNotPauseAuthOnKeyguard() {
207         mController.onViewAttached();
208         captureStatusBarStateListeners();
209 
210         sendStatusBarStateChanged(StatusBarState.KEYGUARD);
211 
212         assertFalse(mController.shouldPauseAuth());
213     }
214 
215     @Test
testShouldPauseAuthIsLaunchTransitionFadingAway()216     public void testShouldPauseAuthIsLaunchTransitionFadingAway() {
217         // GIVEN view is attached and we're on the keyguard (see testShouldNotPauseAuthOnKeyguard)
218         mController.onViewAttached();
219         captureStatusBarStateListeners();
220         sendStatusBarStateChanged(StatusBarState.KEYGUARD);
221 
222         // WHEN isLaunchTransitionFadingAway=true
223         captureKeyguardStateControllerCallback();
224         when(mKeyguardStateController.isLaunchTransitionFadingAway()).thenReturn(true);
225         mKeyguardStateControllerCallback.onLaunchTransitionFadingAwayChanged();
226 
227         // THEN pause auth
228         assertTrue(mController.shouldPauseAuth());
229     }
230 
231     @Test
testShouldPauseAuthOnShadeLocked()232     public void testShouldPauseAuthOnShadeLocked() {
233         mController.onViewAttached();
234         captureStatusBarStateListeners();
235 
236         sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED);
237 
238         assertTrue(mController.shouldPauseAuth());
239     }
240 
241     @Test
testShouldPauseAuthOnShade()242     public void testShouldPauseAuthOnShade() {
243         mController.onViewAttached();
244         captureStatusBarStateListeners();
245 
246         // WHEN not on keyguard yet (shade = home)
247         sendStatusBarStateChanged(StatusBarState.SHADE);
248 
249         // THEN pause auth
250         assertTrue(mController.shouldPauseAuth());
251     }
252 
253     @Test
testShouldPauseAuthAnimatingScreenOffFromShade()254     public void testShouldPauseAuthAnimatingScreenOffFromShade() {
255         mController.onViewAttached();
256         captureStatusBarStateListeners();
257 
258         // WHEN transitioning from home/shade => keyguard + animating screen off
259         mStatusBarStateListener.onStatePreChange(StatusBarState.SHADE, StatusBarState.KEYGUARD);
260         when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(true);
261 
262         // THEN pause auth
263         assertTrue(mController.shouldPauseAuth());
264     }
265 
266     @Test
testDoNotPauseAuthAnimatingScreenOffFromLS()267     public void testDoNotPauseAuthAnimatingScreenOffFromLS() {
268         mController.onViewAttached();
269         captureStatusBarStateListeners();
270 
271         // WHEN animating screen off transition from LS => AOD
272         sendStatusBarStateChanged(StatusBarState.KEYGUARD);
273         when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(true);
274 
275         // THEN don't pause auth
276         assertFalse(mController.shouldPauseAuth());
277     }
278 
279     @Test
testOverrideShouldPauseAuthOnShadeLocked()280     public void testOverrideShouldPauseAuthOnShadeLocked() {
281         mController.onViewAttached();
282         captureStatusBarStateListeners();
283         captureAltAuthInterceptor();
284 
285         sendStatusBarStateChanged(StatusBarState.SHADE_LOCKED);
286         assertTrue(mController.shouldPauseAuth());
287 
288         mAltAuthInterceptor.showAlternateAuthBouncer(); // force show
289         assertFalse(mController.shouldPauseAuth());
290         assertTrue(mAltAuthInterceptor.isShowingAlternateAuthBouncer());
291 
292         mAltAuthInterceptor.hideAlternateAuthBouncer(); // stop force show
293         assertTrue(mController.shouldPauseAuth());
294         assertFalse(mAltAuthInterceptor.isShowingAlternateAuthBouncer());
295     }
296 
297     @Test
testOnDetachedStateReset()298     public void testOnDetachedStateReset() {
299         // GIVEN view is attached
300         mController.onViewAttached();
301         captureAltAuthInterceptor();
302 
303         // WHEN view is detached
304         mController.onViewDetached();
305 
306         // THEN remove alternate auth interceptor
307         verify(mStatusBarKeyguardViewManager).removeAlternateAuthInterceptor(mAltAuthInterceptor);
308     }
309 
310     @Test
testHiddenUdfpsBouncerOnTouchOutside_nothingHappens()311     public void testHiddenUdfpsBouncerOnTouchOutside_nothingHappens() {
312         // GIVEN view is attached
313         mController.onViewAttached();
314         captureAltAuthInterceptor();
315 
316         // GIVEN udfps bouncer isn't showing
317         mAltAuthInterceptor.hideAlternateAuthBouncer();
318 
319         // WHEN touch is observed outside the view
320         mController.onTouchOutsideView();
321 
322         // THEN bouncer / alt auth methods are never called
323         verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
324         verify(mStatusBarKeyguardViewManager, never()).resetAlternateAuth(anyBoolean());
325     }
326 
327     @Test
testShowingUdfpsBouncerOnTouchOutsideWithinThreshold_nothingHappens()328     public void testShowingUdfpsBouncerOnTouchOutsideWithinThreshold_nothingHappens() {
329         // GIVEN view is attached
330         mController.onViewAttached();
331         captureAltAuthInterceptor();
332 
333         // GIVEN udfps bouncer is showing
334         mAltAuthInterceptor.showAlternateAuthBouncer();
335 
336         // WHEN touch is observed outside the view 200ms later (just within threshold)
337         mSystemClock.advanceTime(200);
338         mController.onTouchOutsideView();
339 
340         // THEN bouncer / alt auth methods are never called because not enough time has passed
341         verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
342         verify(mStatusBarKeyguardViewManager, never()).resetAlternateAuth(anyBoolean());
343     }
344 
345     @Test
testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showInputBouncer()346     public void testShowingUdfpsBouncerOnTouchOutsideAboveThreshold_showInputBouncer() {
347         // GIVEN view is attached
348         mController.onViewAttached();
349         captureAltAuthInterceptor();
350 
351         // GIVEN udfps bouncer is showing
352         mAltAuthInterceptor.showAlternateAuthBouncer();
353 
354         // WHEN touch is observed outside the view 205ms later
355         mSystemClock.advanceTime(205);
356         mController.onTouchOutsideView();
357 
358         // THEN show the bouncer
359         verify(mStatusBarKeyguardViewManager).showBouncer(eq(true));
360     }
361 
362     @Test
testFadeInWithStatusBarExpansion()363     public void testFadeInWithStatusBarExpansion() {
364         // GIVEN view is attached
365         mController.onViewAttached();
366         captureExpansionListeners();
367         captureKeyguardStateControllerCallback();
368         reset(mView);
369 
370         // WHEN status bar expansion is 0
371         updateStatusBarExpansion(0, true);
372 
373         // THEN alpha is 0
374         verify(mView).setUnpausedAlpha(0);
375     }
376 
377     @Test
testShowUdfpsBouncer()378     public void testShowUdfpsBouncer() {
379         // GIVEN view is attached and status bar expansion is 0
380         mController.onViewAttached();
381         captureExpansionListeners();
382         captureKeyguardStateControllerCallback();
383         captureAltAuthInterceptor();
384         updateStatusBarExpansion(0, true);
385         reset(mView);
386         when(mView.getContext()).thenReturn(mResourceContext);
387         when(mResourceContext.getString(anyInt())).thenReturn("test string");
388 
389         // WHEN status bar expansion is 0 but udfps bouncer is requested
390         mAltAuthInterceptor.showAlternateAuthBouncer();
391 
392         // THEN alpha is 255
393         verify(mView).setUnpausedAlpha(255);
394     }
395 
396     @Test
testTransitionToFullShadeProgress()397     public void testTransitionToFullShadeProgress() {
398         // GIVEN view is attached and status bar expansion is 1f
399         mController.onViewAttached();
400         captureExpansionListeners();
401         updateStatusBarExpansion(1f, true);
402         reset(mView);
403 
404         // WHEN we're transitioning to the full shade
405         float transitionProgress = .6f;
406         mController.setTransitionToFullShadeProgress(transitionProgress);
407 
408         // THEN alpha is between 0 and 255
409         verify(mView).setUnpausedAlpha((int) ((1f - transitionProgress) * 255));
410     }
411 
412     @Test
testShowUdfpsBouncer_transitionToFullShadeProgress()413     public void testShowUdfpsBouncer_transitionToFullShadeProgress() {
414         // GIVEN view is attached and status bar expansion is 1f
415         mController.onViewAttached();
416         captureExpansionListeners();
417         captureKeyguardStateControllerCallback();
418         captureAltAuthInterceptor();
419         updateStatusBarExpansion(1f, true);
420         mAltAuthInterceptor.showAlternateAuthBouncer();
421         reset(mView);
422 
423         // WHEN we're transitioning to the full shade
424         mController.setTransitionToFullShadeProgress(1.0f);
425 
426         // THEN alpha is 255 (b/c udfps bouncer is requested)
427         verify(mView).setUnpausedAlpha(255);
428     }
429 
sendStatusBarStateChanged(int statusBarState)430     private void sendStatusBarStateChanged(int statusBarState) {
431         mStatusBarStateListener.onStateChanged(statusBarState);
432     }
433 
captureStatusBarStateListeners()434     private void captureStatusBarStateListeners() {
435         verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture());
436         mStatusBarStateListener = mStateListenerCaptor.getValue();
437     }
438 
captureExpansionListeners()439     private void captureExpansionListeners() {
440         verify(mPanelExpansionStateManager, times(2))
441                 .addExpansionListener(mExpansionListenerCaptor.capture());
442         // first (index=0) is from super class, UdfpsAnimationViewController.
443         // second (index=1) is from UdfpsKeyguardViewController
444         mExpansionListeners = mExpansionListenerCaptor.getAllValues();
445     }
446 
updateStatusBarExpansion(float fraction, boolean expanded)447     private void updateStatusBarExpansion(float fraction, boolean expanded) {
448         for (PanelExpansionListener listener : mExpansionListeners) {
449             listener.onPanelExpansionChanged(fraction, expanded, /* tracking= */ false);
450         }
451     }
452 
captureAltAuthInterceptor()453     private void captureAltAuthInterceptor() {
454         verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor(
455                 mAltAuthInterceptorCaptor.capture());
456         mAltAuthInterceptor = mAltAuthInterceptorCaptor.getValue();
457     }
458 
459 
460 
captureKeyguardStateControllerCallback()461     private void captureKeyguardStateControllerCallback() {
462         verify(mKeyguardStateController).addCallback(
463                 mKeyguardStateControllerCallbackCaptor.capture());
464         mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue();
465     }
466 }
467