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.animation.Animator
20 import android.app.ActivityManager
21 import android.app.ActivityTaskManager
22 import android.content.ComponentName
23 import android.graphics.Insets
24 import android.graphics.Rect
25 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
26 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
27 import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
28 import android.hardware.biometrics.SensorLocationInternal
29 import android.hardware.biometrics.SensorProperties
30 import android.hardware.display.DisplayManager
31 import android.hardware.display.DisplayManagerGlobal
32 import android.hardware.fingerprint.FingerprintManager
33 import android.hardware.fingerprint.FingerprintSensorProperties
34 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
35 import android.hardware.fingerprint.ISidefpsController
36 import android.os.Handler
37 import android.testing.TestableLooper
38 import android.view.Display
39 import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS
40 import android.view.DisplayInfo
41 import android.view.LayoutInflater
42 import android.view.Surface
43 import android.view.View
44 import android.view.ViewPropertyAnimator
45 import android.view.WindowInsets
46 import android.view.WindowManager
47 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
48 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
49 import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
50 import android.view.WindowMetrics
51 import androidx.test.ext.junit.runners.AndroidJUnit4
52 import androidx.test.filters.SmallTest
53 import com.airbnb.lottie.LottieAnimationView
54 import com.android.keyguard.KeyguardUpdateMonitor
55 import com.android.systemui.R
56 import com.android.systemui.RoboPilotTest
57 import com.android.systemui.SysuiTestCase
58 import com.android.systemui.SysuiTestableContext
59 import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
60 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
61 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl
62 import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
63 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
64 import com.android.systemui.dump.DumpManager
65 import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
66 import com.android.systemui.plugins.statusbar.StatusBarStateController
67 import com.android.systemui.statusbar.policy.KeyguardStateController
68 import com.android.systemui.util.concurrency.FakeExecutor
69 import com.android.systemui.util.time.FakeSystemClock
70 import com.google.common.truth.Truth.assertThat
71 import kotlinx.coroutines.test.StandardTestDispatcher
72 import kotlinx.coroutines.test.TestCoroutineScope
73 import kotlinx.coroutines.test.TestScope
74 import org.junit.Before
75 import org.junit.Rule
76 import org.junit.Test
77 import org.junit.runner.RunWith
78 import org.mockito.ArgumentCaptor
79 import org.mockito.ArgumentMatchers.eq
80 import org.mockito.Captor
81 import org.mockito.Mock
82 import org.mockito.Mockito.any
83 import org.mockito.Mockito.anyFloat
84 import org.mockito.Mockito.anyInt
85 import org.mockito.Mockito.anyLong
86 import org.mockito.Mockito.mock
87 import org.mockito.Mockito.never
88 import org.mockito.Mockito.reset
89 import org.mockito.Mockito.times
90 import org.mockito.Mockito.verify
91 import org.mockito.Mockito.`when` as whenEver
92 import org.mockito.junit.MockitoJUnit
93 
94 private const val DISPLAY_ID = 2
95 private const val SENSOR_ID = 1
96 
97 private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3
98 
99 @SmallTest
100 @RoboPilotTest
101 @RunWith(AndroidJUnit4::class)
102 @TestableLooper.RunWithLooper
103 class SideFpsControllerTest : SysuiTestCase() {
104 
105     @JvmField @Rule var rule = MockitoJUnit.rule()
106 
107     @Mock lateinit var layoutInflater: LayoutInflater
108     @Mock lateinit var fingerprintManager: FingerprintManager
109     @Mock lateinit var windowManager: WindowManager
110     @Mock lateinit var activityTaskManager: ActivityTaskManager
111     @Mock lateinit var sideFpsView: View
112     @Mock lateinit var displayManager: DisplayManager
113     @Mock lateinit var handler: Handler
114     @Mock lateinit var dumpManager: DumpManager
115     @Captor lateinit var overlayCaptor: ArgumentCaptor<View>
116     @Captor lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
117 
118     private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository
119     private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor
120     private lateinit var displayStateInteractor: DisplayStateInteractor
121 
122     private val executor = FakeExecutor(FakeSystemClock())
123     private val displayStateRepository = FakeDisplayStateRepository()
124     private val testScope = TestScope(StandardTestDispatcher())
125 
126     private lateinit var overlayController: ISidefpsController
127     private lateinit var sideFpsController: SideFpsController
128 
129     enum class DeviceConfig {
130         X_ALIGNED,
131         Y_ALIGNED,
132     }
133 
134     private lateinit var deviceConfig: DeviceConfig
135     private lateinit var indicatorBounds: Rect
136     private lateinit var displayBounds: Rect
137     private lateinit var sensorLocation: SensorLocationInternal
138     private var displayWidth: Int = 0
139     private var displayHeight: Int = 0
140     private var boundsWidth: Int = 0
141     private var boundsHeight: Int = 0
142 
143     @Before
144     fun setup() {
145         keyguardBouncerRepository = FakeKeyguardBouncerRepository()
146         alternateBouncerInteractor =
147             AlternateBouncerInteractor(
148                 mock(StatusBarStateController::class.java),
149                 mock(KeyguardStateController::class.java),
150                 keyguardBouncerRepository,
151                 FakeBiometricSettingsRepository(),
152                 FakeSystemClock(),
153                 mock(KeyguardUpdateMonitor::class.java),
154             )
155         displayStateInteractor =
156             DisplayStateInteractorImpl(
157                 testScope.backgroundScope,
158                 context,
159                 executor,
160                 displayStateRepository
161             )
162 
163         context.addMockSystemService(DisplayManager::class.java, displayManager)
164         context.addMockSystemService(WindowManager::class.java, windowManager)
165 
166         whenEver(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sideFpsView)
167         whenEver(sideFpsView.requireViewById<LottieAnimationView>(eq(R.id.sidefps_animation)))
168             .thenReturn(mock(LottieAnimationView::class.java))
169         with(mock(ViewPropertyAnimator::class.java)) {
170             whenEver(sideFpsView.animate()).thenReturn(this)
171             whenEver(alpha(anyFloat())).thenReturn(this)
172             whenEver(setStartDelay(anyLong())).thenReturn(this)
173             whenEver(setDuration(anyLong())).thenReturn(this)
174             whenEver(setListener(any())).thenAnswer {
175                 (it.arguments[0] as Animator.AnimatorListener).onAnimationEnd(
176                     mock(Animator::class.java)
177                 )
178                 this
179             }
180         }
181     }
182 
183     private fun testWithDisplay(
184         deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED,
185         isReverseDefaultRotation: Boolean = false,
186         initInfo: DisplayInfo.() -> Unit = {},
187         windowInsets: WindowInsets = insetsForSmallNavbar(),
188         inRearDisplayMode: Boolean = false,
189         block: () -> Unit
190     ) {
191         this.deviceConfig = deviceConfig
192 
193         when (deviceConfig) {
194             DeviceConfig.X_ALIGNED -> {
195                 displayWidth = 3000
196                 displayHeight = 1500
197                 sensorLocation = SensorLocationInternal("", 2500, 0, 0)
198                 boundsWidth = 200
199                 boundsHeight = 100
200             }
201             DeviceConfig.Y_ALIGNED -> {
202                 displayWidth = 2500
203                 displayHeight = 2000
204                 sensorLocation = SensorLocationInternal("", 0, 300, 0)
205                 boundsWidth = 100
206                 boundsHeight = 200
207             }
208         }
209 
210         indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight)
211         displayBounds = Rect(0, 0, displayWidth, displayHeight)
212         var locations = listOf(sensorLocation)
213 
214         whenEver(fingerprintManager.sensorPropertiesInternal)
215             .thenReturn(
216                 listOf(
217                     FingerprintSensorPropertiesInternal(
218                         SENSOR_ID,
219                         SensorProperties.STRENGTH_STRONG,
220                         5 /* maxEnrollmentsPerUser */,
221                         listOf() /* componentInfo */,
222                         FingerprintSensorProperties.TYPE_POWER_BUTTON,
223                         true /* halControlsIllumination */,
224                         true /* resetLockoutRequiresHardwareAuthToken */,
225                         locations
226                     )
227                 )
228             )
229 
230         val displayInfo = DisplayInfo()
231         displayInfo.initInfo()
232 
233         val dmGlobal = mock(DisplayManagerGlobal::class.java)
234         val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS)
235 
236         whenEver(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo)
237         whenEver(windowManager.defaultDisplay).thenReturn(display)
238         whenEver(windowManager.maximumWindowMetrics)
239             .thenReturn(WindowMetrics(displayBounds, WindowInsets.CONSUMED))
240         whenEver(windowManager.currentWindowMetrics)
241             .thenReturn(WindowMetrics(displayBounds, windowInsets))
242 
243         val sideFpsControllerContext = context.createDisplayContext(display) as SysuiTestableContext
244         sideFpsControllerContext.orCreateTestableResources.addOverride(
245             com.android.internal.R.bool.config_reverseDefaultRotation,
246             isReverseDefaultRotation
247         )
248 
249         val rearDisplayDeviceStates =
250             if (inRearDisplayMode) intArrayOf(REAR_DISPLAY_MODE_DEVICE_STATE) else intArrayOf()
251         sideFpsControllerContext.orCreateTestableResources.addOverride(
252             com.android.internal.R.array.config_rearDisplayDeviceStates,
253             rearDisplayDeviceStates
254         )
255 
256         sideFpsController =
257             SideFpsController(
258                 sideFpsControllerContext,
259                 layoutInflater,
260                 fingerprintManager,
261                 windowManager,
262                 activityTaskManager,
263                 displayManager,
264                 displayStateInteractor,
265                 executor,
266                 handler,
267                 alternateBouncerInteractor,
268                 TestCoroutineScope(),
269                 dumpManager
270             )
271         displayStateRepository.setIsInRearDisplayMode(inRearDisplayMode)
272 
273         overlayController =
274             ArgumentCaptor.forClass(ISidefpsController::class.java)
275                 .apply { verify(fingerprintManager).setSidefpsController(capture()) }
276                 .value
277 
278         block()
279     }
280 
281     @Test
282     fun testSubscribesToOrientationChangesWhenShowingOverlay() = testWithDisplay {
283         overlayController.show(SENSOR_ID, REASON_UNKNOWN)
284         executor.runAllReady()
285 
286         verify(displayManager).registerDisplayListener(any(), eq(handler), anyLong())
287 
288         overlayController.hide(SENSOR_ID)
289         executor.runAllReady()
290         verify(displayManager).unregisterDisplayListener(any())
291     }
292 
293     @Test
294     fun testShowOverlayReasonWhenDisplayChanged() = testWithDisplay {
295         sideFpsController.show(SideFpsUiRequestSource.AUTO_SHOW, REASON_AUTH_KEYGUARD)
296         executor.runAllReady()
297         sideFpsController.orientationListener.onDisplayChanged(1 /* displayId */)
298         executor.runAllReady()
299 
300         assertThat(sideFpsController.orientationReasonListener.reason)
301             .isEqualTo(REASON_AUTH_KEYGUARD)
302     }
303 
304     @Test
305     fun testShowsAndHides() = testWithDisplay {
306         overlayController.show(SENSOR_ID, REASON_UNKNOWN)
307         executor.runAllReady()
308 
309         verify(windowManager).addView(overlayCaptor.capture(), any())
310 
311         reset(windowManager)
312         overlayController.hide(SENSOR_ID)
313         executor.runAllReady()
314 
315         verify(windowManager, never()).addView(any(), any())
316         verify(windowManager).removeView(eq(overlayCaptor.value))
317     }
318 
319     @Test
320     fun testShowsOnce() = testWithDisplay {
321         repeat(5) {
322             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
323             executor.runAllReady()
324         }
325 
326         verify(windowManager).addView(any(), any())
327         verify(windowManager, never()).removeView(any())
328     }
329 
330     @Test
331     fun testHidesOnce() = testWithDisplay {
332         overlayController.show(SENSOR_ID, REASON_UNKNOWN)
333         executor.runAllReady()
334 
335         repeat(5) {
336             overlayController.hide(SENSOR_ID)
337             executor.runAllReady()
338         }
339 
340         verify(windowManager).addView(any(), any())
341         verify(windowManager).removeView(any())
342     }
343 
344     @Test fun testIgnoredForKeyguard() = testWithDisplay { testIgnoredFor(REASON_AUTH_KEYGUARD) }
345 
346     @Test
347     fun testShowsForMostSettings() = testWithDisplay {
348         whenEver(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpEnrollTask()))
349         testIgnoredFor(REASON_AUTH_SETTINGS, ignored = false)
350     }
351 
352     @Test
353     fun testIgnoredForVerySpecificSettings() = testWithDisplay {
354         whenEver(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpSettingsTask()))
355         testIgnoredFor(REASON_AUTH_SETTINGS)
356     }
357 
358     private fun testIgnoredFor(reason: Int, ignored: Boolean = true) {
359         overlayController.show(SENSOR_ID, reason)
360         executor.runAllReady()
361 
362         verify(windowManager, if (ignored) never() else times(1)).addView(any(), any())
363     }
364 
365     @Test
366     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_0() =
367         testWithDisplay(
368             deviceConfig = DeviceConfig.X_ALIGNED,
369             isReverseDefaultRotation = false,
370             { rotation = Surface.ROTATION_0 }
371         ) {
372             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
373         }
374 
375     @Test
376     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_90() =
377         testWithDisplay(
378             deviceConfig = DeviceConfig.X_ALIGNED,
379             isReverseDefaultRotation = false,
380             { rotation = Surface.ROTATION_90 }
381         ) {
382             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
383         }
384 
385     @Test
386     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_180() =
387         testWithDisplay(
388             deviceConfig = DeviceConfig.X_ALIGNED,
389             isReverseDefaultRotation = false,
390             { rotation = Surface.ROTATION_180 }
391         ) {
392             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
393         }
394 
395     @Test
396     fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_180() =
397         testWithDisplay(
398             deviceConfig = DeviceConfig.X_ALIGNED,
399             isReverseDefaultRotation = false,
400             { rotation = Surface.ROTATION_180 },
401             windowInsets = insetsForSmallNavbar()
402         ) {
403             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
404         }
405 
406     @Test
407     fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_180() =
408         testWithDisplay(
409             deviceConfig = DeviceConfig.X_ALIGNED,
410             isReverseDefaultRotation = false,
411             { rotation = Surface.ROTATION_180 },
412             windowInsets = insetsForLargeNavbar()
413         ) {
414             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false)
415         }
416 
417     @Test
418     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_270() =
419         testWithDisplay(
420             deviceConfig = DeviceConfig.X_ALIGNED,
421             isReverseDefaultRotation = false,
422             { rotation = Surface.ROTATION_270 }
423         ) {
424             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
425         }
426 
427     @Test
428     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_0() =
429         testWithDisplay(
430             deviceConfig = DeviceConfig.X_ALIGNED,
431             isReverseDefaultRotation = true,
432             { rotation = Surface.ROTATION_0 }
433         ) {
434             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
435         }
436 
437     @Test
438     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_90() =
439         testWithDisplay(
440             deviceConfig = DeviceConfig.X_ALIGNED,
441             isReverseDefaultRotation = true,
442             { rotation = Surface.ROTATION_90 }
443         ) {
444             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
445         }
446 
447     @Test
448     fun showsSfpsIndicatorWithTaskbarCollapsedDownForXAlignedSensor_InReverseDefaultRotation_90() =
449         testWithDisplay(
450             deviceConfig = DeviceConfig.X_ALIGNED,
451             isReverseDefaultRotation = true,
452             { rotation = Surface.ROTATION_90 },
453             windowInsets = insetsForSmallNavbar()
454         ) {
455             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
456         }
457 
458     @Test
459     fun hidesSfpsIndicatorWhenOccludingTaskbarForXAlignedSensor_InReverseDefaultRotation_90() =
460         testWithDisplay(
461             deviceConfig = DeviceConfig.X_ALIGNED,
462             isReverseDefaultRotation = true,
463             { rotation = Surface.ROTATION_90 },
464             windowInsets = insetsForLargeNavbar()
465         ) {
466             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false)
467         }
468 
469     @Test
470     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_180() =
471         testWithDisplay(
472             deviceConfig = DeviceConfig.X_ALIGNED,
473             isReverseDefaultRotation = true,
474             { rotation = Surface.ROTATION_180 }
475         ) {
476             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
477         }
478 
479     @Test
480     fun showsSfpsIndicatorWithTaskbarForXAlignedSensor_InReverseDefaultRotation_270() =
481         testWithDisplay(
482             deviceConfig = DeviceConfig.X_ALIGNED,
483             isReverseDefaultRotation = true,
484             { rotation = Surface.ROTATION_270 }
485         ) {
486             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
487         }
488 
489     @Test
490     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_0() =
491         testWithDisplay(
492             deviceConfig = DeviceConfig.Y_ALIGNED,
493             isReverseDefaultRotation = false,
494             { rotation = Surface.ROTATION_0 }
495         ) {
496             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
497         }
498 
499     @Test
500     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_90() =
501         testWithDisplay(
502             deviceConfig = DeviceConfig.Y_ALIGNED,
503             isReverseDefaultRotation = false,
504             { rotation = Surface.ROTATION_90 }
505         ) {
506             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
507         }
508 
509     @Test
510     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_180() =
511         testWithDisplay(
512             deviceConfig = DeviceConfig.Y_ALIGNED,
513             isReverseDefaultRotation = false,
514             { rotation = Surface.ROTATION_180 },
515         ) {
516             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
517         }
518 
519     @Test
520     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_270() =
521         testWithDisplay(
522             deviceConfig = DeviceConfig.Y_ALIGNED,
523             isReverseDefaultRotation = false,
524             { rotation = Surface.ROTATION_270 }
525         ) {
526             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
527         }
528 
529     @Test
530     fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_270() =
531         testWithDisplay(
532             deviceConfig = DeviceConfig.Y_ALIGNED,
533             isReverseDefaultRotation = false,
534             { rotation = Surface.ROTATION_270 },
535             windowInsets = insetsForSmallNavbar()
536         ) {
537             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
538         }
539 
540     @Test
541     fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_270() =
542         testWithDisplay(
543             deviceConfig = DeviceConfig.Y_ALIGNED,
544             isReverseDefaultRotation = false,
545             { rotation = Surface.ROTATION_270 },
546             windowInsets = insetsForLargeNavbar()
547         ) {
548             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false)
549         }
550 
551     @Test
552     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_0() =
553         testWithDisplay(
554             deviceConfig = DeviceConfig.Y_ALIGNED,
555             isReverseDefaultRotation = true,
556             { rotation = Surface.ROTATION_0 }
557         ) {
558             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
559         }
560 
561     @Test
562     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_90() =
563         testWithDisplay(
564             deviceConfig = DeviceConfig.Y_ALIGNED,
565             isReverseDefaultRotation = true,
566             { rotation = Surface.ROTATION_90 },
567         ) {
568             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
569         }
570 
571     @Test
572     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_180() =
573         testWithDisplay(
574             deviceConfig = DeviceConfig.Y_ALIGNED,
575             isReverseDefaultRotation = true,
576             { rotation = Surface.ROTATION_180 }
577         ) {
578             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
579         }
580 
581     @Test
582     fun showsSfpsIndicatorWithTaskbarCollapsedDownForYAlignedSensor_InReverseDefaultRotation_180() =
583         testWithDisplay(
584             deviceConfig = DeviceConfig.Y_ALIGNED,
585             isReverseDefaultRotation = true,
586             { rotation = Surface.ROTATION_180 },
587             windowInsets = insetsForSmallNavbar()
588         ) {
589             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
590         }
591 
592     @Test
593     fun hidesSfpsIndicatorWhenOccludingTaskbarForYAlignedSensor_InReverseDefaultRotation_180() =
594         testWithDisplay(
595             deviceConfig = DeviceConfig.Y_ALIGNED,
596             isReverseDefaultRotation = true,
597             { rotation = Surface.ROTATION_180 },
598             windowInsets = insetsForLargeNavbar()
599         ) {
600             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = false)
601         }
602 
603     @Test
604     fun showsSfpsIndicatorWithTaskbarForYAlignedSensor_InReverseDefaultRotation_270() =
605         testWithDisplay(
606             deviceConfig = DeviceConfig.Y_ALIGNED,
607             isReverseDefaultRotation = true,
608             { rotation = Surface.ROTATION_270 }
609         ) {
610             verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible = true)
611         }
612 
613     @Test
614     fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_0() =
615         testWithDisplay(
616             deviceConfig = DeviceConfig.Y_ALIGNED,
617             isReverseDefaultRotation = false,
618             { rotation = Surface.ROTATION_0 },
619             inRearDisplayMode = true,
620         ) {
621             verifySfpsIndicator_notAdded_InRearDisplayMode()
622         }
623 
624     @Test
625     fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_90() =
626         testWithDisplay(
627             deviceConfig = DeviceConfig.Y_ALIGNED,
628             isReverseDefaultRotation = false,
629             { rotation = Surface.ROTATION_90 },
630             inRearDisplayMode = true,
631         ) {
632             verifySfpsIndicator_notAdded_InRearDisplayMode()
633         }
634 
635     @Test
636     fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_180() =
637         testWithDisplay(
638             deviceConfig = DeviceConfig.Y_ALIGNED,
639             isReverseDefaultRotation = false,
640             { rotation = Surface.ROTATION_180 },
641             inRearDisplayMode = true,
642         ) {
643             verifySfpsIndicator_notAdded_InRearDisplayMode()
644         }
645 
646     @Test
647     fun verifiesSfpsIndicatorNotAddedInRearDisplayMode_270() =
648         testWithDisplay(
649             deviceConfig = DeviceConfig.Y_ALIGNED,
650             isReverseDefaultRotation = false,
651             { rotation = Surface.ROTATION_270 },
652             inRearDisplayMode = true,
653         ) {
654             verifySfpsIndicator_notAdded_InRearDisplayMode()
655         }
656 
657     private fun verifySfpsIndicatorVisibilityOnTaskbarUpdate(sfpsViewVisible: Boolean) {
658         sideFpsController.overlayOffsets = sensorLocation
659     }
660 
661     private fun verifySfpsIndicator_notAdded_InRearDisplayMode() {
662         sideFpsController.overlayOffsets = sensorLocation
663         overlayController.show(SENSOR_ID, REASON_UNKNOWN)
664         executor.runAllReady()
665 
666         verify(windowManager, never()).addView(any(), any())
667     }
668 
669     fun alternateBouncerVisibility_showAndHideSideFpsUI() = testWithDisplay {
670         // WHEN alternate bouncer is visible
671         keyguardBouncerRepository.setAlternateVisible(true)
672         executor.runAllReady()
673 
674         // THEN side fps shows UI
675         verify(windowManager).addView(any(), any())
676         verify(windowManager, never()).removeView(any())
677 
678         // WHEN alternate bouncer is no longer visible
679         keyguardBouncerRepository.setAlternateVisible(false)
680         executor.runAllReady()
681 
682         // THEN side fps UI is hidden
683         verify(windowManager).removeView(any())
684     }
685 
686     /**
687      * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
688      * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
689      * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
690      * in other rotations have been omitted.
691      */
692     @Test
693     fun verifiesIndicatorPlacementForXAlignedSensor_0() =
694         testWithDisplay(
695             deviceConfig = DeviceConfig.X_ALIGNED,
696             isReverseDefaultRotation = false,
697             { rotation = Surface.ROTATION_0 }
698         ) {
699             sideFpsController.overlayOffsets = sensorLocation
700 
701             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
702 
703             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
704             executor.runAllReady()
705 
706             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
707             assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
708             assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
709         }
710 
711     /**
712      * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
713      * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
714      * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
715      * correctly, tests for indicator placement in other rotations have been omitted.
716      */
717     @Test
718     fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() =
719         testWithDisplay(
720             deviceConfig = DeviceConfig.X_ALIGNED,
721             isReverseDefaultRotation = true,
722             { rotation = Surface.ROTATION_270 }
723         ) {
724             sideFpsController.overlayOffsets = sensorLocation
725 
726             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
727 
728             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
729             executor.runAllReady()
730 
731             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
732             assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX)
733             assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0)
734         }
735 
736     /**
737      * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0,
738      * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device
739      * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement
740      * in other rotations have been omitted.
741      */
742     @Test
743     fun verifiesIndicatorPlacementForYAlignedSensor_0() =
744         testWithDisplay(
745             deviceConfig = DeviceConfig.Y_ALIGNED,
746             isReverseDefaultRotation = false,
747             { rotation = Surface.ROTATION_0 }
748         ) {
749             sideFpsController.overlayOffsets = sensorLocation
750 
751             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
752 
753             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
754             executor.runAllReady()
755 
756             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
757             assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
758             assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
759         }
760 
761     /**
762      * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270
763      * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct
764      * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works
765      * correctly, tests for indicator placement in other rotations have been omitted.
766      */
767     @Test
768     fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() =
769         testWithDisplay(
770             deviceConfig = DeviceConfig.Y_ALIGNED,
771             isReverseDefaultRotation = true,
772             { rotation = Surface.ROTATION_270 }
773         ) {
774             sideFpsController.overlayOffsets = sensorLocation
775 
776             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
777 
778             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
779             executor.runAllReady()
780 
781             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
782             assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth)
783             assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY)
784         }
785 
786     @Test
787     fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay {
788         // By default all those tests assume the side fps sensor is available.
789         assertThat(fingerprintManager.hasSideFpsSensor()).isTrue()
790     }
791 
792     @Test
793     fun hasSideFpsSensor_withoutSensorProps_returnsFalse() {
794         whenEver(fingerprintManager.sensorPropertiesInternal).thenReturn(null)
795 
796         assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
797     }
798 
799     @Test
800     fun testLayoutParams_isKeyguardDialogType() =
801         testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
802             sideFpsController.overlayOffsets = sensorLocation
803             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
804             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
805             executor.runAllReady()
806 
807             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
808 
809             val lpType = overlayViewParamsCaptor.value.type
810 
811             assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue()
812         }
813 
814     @Test
815     fun testLayoutParams_hasNoMoveAnimationWindowFlag() =
816         testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
817             sideFpsController.overlayOffsets = sensorLocation
818             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
819             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
820             executor.runAllReady()
821 
822             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
823 
824             val lpFlags = overlayViewParamsCaptor.value.privateFlags
825 
826             assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
827         }
828 
829     @Test
830     fun testLayoutParams_hasTrustedOverlayWindowFlag() =
831         testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) {
832             sideFpsController.overlayOffsets = sensorLocation
833             sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds)
834             overlayController.show(SENSOR_ID, REASON_UNKNOWN)
835             executor.runAllReady()
836 
837             verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
838 
839             val lpFlags = overlayViewParamsCaptor.value.privateFlags
840 
841             assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
842         }
843 
844     @Test
845     fun primaryBouncerRequestAnimatesAlphaIn() = testWithDisplay {
846         sideFpsController.show(SideFpsUiRequestSource.PRIMARY_BOUNCER, REASON_AUTH_KEYGUARD)
847         executor.runAllReady()
848         verify(sideFpsView).animate()
849     }
850 
851     @Test
852     fun alternateBouncerRequestsDoesNotAnimateAlphaIn() = testWithDisplay {
853         sideFpsController.show(SideFpsUiRequestSource.ALTERNATE_BOUNCER, REASON_AUTH_KEYGUARD)
854         executor.runAllReady()
855         verify(sideFpsView, never()).animate()
856     }
857 
858     @Test
859     fun autoShowRequestsDoesNotAnimateAlphaIn() = testWithDisplay {
860         sideFpsController.show(SideFpsUiRequestSource.AUTO_SHOW, REASON_AUTH_KEYGUARD)
861         executor.runAllReady()
862         verify(sideFpsView, never()).animate()
863     }
864 }
865 
866 private fun insetsForSmallNavbar() = insetsWithBottom(60)
867 
868 private fun insetsForLargeNavbar() = insetsWithBottom(100)
869 
870 private fun insetsWithBottom(bottom: Int) =
871     WindowInsets.Builder()
872         .setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, bottom))
873         .build()
874 
875 private fun fpEnrollTask() = settingsTask(".biometrics.fingerprint.FingerprintEnrollEnrolling")
876 
877 private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings")
878 
879 private fun settingsTask(cls: String) =
880     ActivityManager.RunningTaskInfo().apply {
881         topActivity = ComponentName.createRelative("com.android.settings", cls)
882     }
883