| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.biometrics |
| |
| import android.animation.Animator |
| import android.app.ActivityManager |
| import android.app.ActivityTaskManager |
| import android.content.ComponentName |
| import android.graphics.Insets |
| import android.graphics.Rect |
| import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD |
| import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS |
| import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN |
| import android.hardware.biometrics.SensorLocationInternal |
| import android.hardware.biometrics.SensorProperties |
| import android.hardware.display.DisplayManager |
| import android.hardware.display.DisplayManagerGlobal |
| import android.hardware.fingerprint.FingerprintManager |
| import android.hardware.fingerprint.FingerprintSensorProperties |
| import android.hardware.fingerprint.FingerprintSensorPropertiesInternal |
| import android.hardware.fingerprint.ISidefpsController |
| import android.os.Handler |
| import android.testing.AndroidTestingRunner |
| import android.testing.TestableLooper |
| import android.view.Display |
| import android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS |
| import android.view.DisplayInfo |
| import android.view.LayoutInflater |
| import android.view.Surface |
| import android.view.View |
| import android.view.ViewPropertyAnimator |
| import android.view.WindowInsets |
| import android.view.WindowManager |
| import android.view.WindowMetrics |
| import androidx.test.filters.SmallTest |
| import com.airbnb.lottie.LottieAnimationView |
| import com.android.systemui.R |
| import com.android.systemui.SysuiTestCase |
| import com.android.systemui.recents.OverviewProxyService |
| import com.android.systemui.util.concurrency.FakeExecutor |
| import com.android.systemui.util.time.FakeSystemClock |
| import com.google.common.truth.Truth.assertThat |
| import org.junit.Before |
| import org.junit.Rule |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.mockito.ArgumentCaptor |
| import org.mockito.ArgumentMatchers.eq |
| import org.mockito.Captor |
| import org.mockito.Mock |
| import org.mockito.Mockito.`when` |
| import org.mockito.Mockito.any |
| import org.mockito.Mockito.anyFloat |
| import org.mockito.Mockito.anyInt |
| import org.mockito.Mockito.anyLong |
| import org.mockito.Mockito.mock |
| import org.mockito.Mockito.never |
| import org.mockito.Mockito.reset |
| import org.mockito.Mockito.times |
| import org.mockito.Mockito.verify |
| import org.mockito.junit.MockitoJUnit |
| |
| private const val DISPLAY_ID = 2 |
| private const val SENSOR_ID = 1 |
| |
| @SmallTest |
| @RunWith(AndroidTestingRunner::class) |
| @TestableLooper.RunWithLooper |
| class SidefpsControllerTest : SysuiTestCase() { |
| |
| @JvmField @Rule |
| var rule = MockitoJUnit.rule() |
| |
| @Mock |
| lateinit var layoutInflater: LayoutInflater |
| @Mock |
| lateinit var fingerprintManager: FingerprintManager |
| @Mock |
| lateinit var windowManager: WindowManager |
| @Mock |
| lateinit var activityTaskManager: ActivityTaskManager |
| @Mock |
| lateinit var sidefpsView: View |
| @Mock |
| lateinit var displayManager: DisplayManager |
| @Mock |
| lateinit var overviewProxyService: OverviewProxyService |
| @Mock |
| lateinit var handler: Handler |
| @Captor |
| lateinit var overlayCaptor: ArgumentCaptor<View> |
| @Captor |
| lateinit var overlayViewParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> |
| |
| private val executor = FakeExecutor(FakeSystemClock()) |
| private lateinit var overlayController: ISidefpsController |
| private lateinit var sideFpsController: SidefpsController |
| |
| enum class DeviceConfig { X_ALIGNED, Y_ALIGNED_UNFOLDED, Y_ALIGNED_FOLDED } |
| |
| private lateinit var deviceConfig: DeviceConfig |
| private lateinit var indicatorBounds: Rect |
| private lateinit var displayBounds: Rect |
| private lateinit var sensorLocation: SensorLocationInternal |
| private var displayWidth: Int = 0 |
| private var displayHeight: Int = 0 |
| private var boundsWidth: Int = 0 |
| private var boundsHeight: Int = 0 |
| |
| @Before |
| fun setup() { |
| context.addMockSystemService(DisplayManager::class.java, displayManager) |
| context.addMockSystemService(WindowManager::class.java, windowManager) |
| |
| `when`(layoutInflater.inflate(R.layout.sidefps_view, null, false)).thenReturn(sidefpsView) |
| `when`(sidefpsView.findViewById<LottieAnimationView>(eq(R.id.sidefps_animation))) |
| .thenReturn(mock(LottieAnimationView::class.java)) |
| with(mock(ViewPropertyAnimator::class.java)) { |
| `when`(sidefpsView.animate()).thenReturn(this) |
| `when`(alpha(anyFloat())).thenReturn(this) |
| `when`(setStartDelay(anyLong())).thenReturn(this) |
| `when`(setDuration(anyLong())).thenReturn(this) |
| `when`(setListener(any())).thenAnswer { |
| (it.arguments[0] as Animator.AnimatorListener) |
| .onAnimationEnd(mock(Animator::class.java)) |
| this |
| } |
| } |
| } |
| |
| private fun testWithDisplay( |
| deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED, |
| initInfo: DisplayInfo.() -> Unit = {}, |
| windowInsets: WindowInsets = insetsForSmallNavbar(), |
| block: () -> Unit |
| ) { |
| this.deviceConfig = deviceConfig |
| |
| when (deviceConfig) { |
| DeviceConfig.X_ALIGNED -> { |
| displayWidth = 2560 |
| displayHeight = 1600 |
| sensorLocation = SensorLocationInternal("", 2325, 0, 0) |
| boundsWidth = 160 |
| boundsHeight = 84 |
| } |
| DeviceConfig.Y_ALIGNED_UNFOLDED -> { |
| displayWidth = 2208 |
| displayHeight = 1840 |
| sensorLocation = SensorLocationInternal("", 0, 510, 0) |
| boundsWidth = 110 |
| boundsHeight = 210 |
| } |
| DeviceConfig.Y_ALIGNED_FOLDED -> { |
| displayWidth = 1080 |
| displayHeight = 2100 |
| sensorLocation = SensorLocationInternal("", 0, 590, 0) |
| boundsWidth = 110 |
| boundsHeight = 210 |
| } |
| } |
| indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight) |
| displayBounds = Rect(0, 0, displayWidth, displayHeight) |
| var locations = listOf(sensorLocation) |
| |
| `when`(fingerprintManager.sensorPropertiesInternal).thenReturn( |
| listOf( |
| FingerprintSensorPropertiesInternal( |
| SENSOR_ID, |
| SensorProperties.STRENGTH_STRONG, |
| 5 /* maxEnrollmentsPerUser */, |
| listOf() /* componentInfo */, |
| FingerprintSensorProperties.TYPE_POWER_BUTTON, |
| true /* halControlsIllumination */, |
| true /* resetLockoutRequiresHardwareAuthToken */, |
| locations |
| ) |
| ) |
| ) |
| |
| val displayInfo = DisplayInfo() |
| displayInfo.initInfo() |
| val dmGlobal = mock(DisplayManagerGlobal::class.java) |
| val display = Display(dmGlobal, DISPLAY_ID, displayInfo, DEFAULT_DISPLAY_ADJUSTMENTS) |
| `when`(dmGlobal.getDisplayInfo(eq(DISPLAY_ID))).thenReturn(displayInfo) |
| `when`(windowManager.defaultDisplay).thenReturn(display) |
| `when`(windowManager.maximumWindowMetrics).thenReturn( |
| WindowMetrics(displayBounds, WindowInsets.CONSUMED) |
| ) |
| `when`(windowManager.currentWindowMetrics).thenReturn( |
| WindowMetrics(displayBounds, windowInsets) |
| ) |
| |
| sideFpsController = SidefpsController( |
| context.createDisplayContext(display), layoutInflater, fingerprintManager, |
| windowManager, activityTaskManager, overviewProxyService, displayManager, executor, |
| handler |
| ) |
| |
| overlayController = ArgumentCaptor.forClass(ISidefpsController::class.java).apply { |
| verify(fingerprintManager).setSidefpsController(capture()) |
| }.value |
| |
| block() |
| } |
| |
| @Test |
| fun testSubscribesToOrientationChangesWhenShowingOverlay() = testWithDisplay { |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| executor.runAllReady() |
| |
| verify(displayManager).registerDisplayListener(any(), eq(handler), anyLong()) |
| |
| overlayController.hide(SENSOR_ID) |
| executor.runAllReady() |
| verify(displayManager).unregisterDisplayListener(any()) |
| } |
| |
| @Test |
| fun testShowsAndHides() = testWithDisplay { |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| executor.runAllReady() |
| |
| verify(windowManager).addView(overlayCaptor.capture(), any()) |
| |
| reset(windowManager) |
| overlayController.hide(SENSOR_ID) |
| executor.runAllReady() |
| |
| verify(windowManager, never()).addView(any(), any()) |
| verify(windowManager).removeView(eq(overlayCaptor.value)) |
| } |
| |
| @Test |
| fun testShowsOnce() = testWithDisplay { |
| repeat(5) { |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| executor.runAllReady() |
| } |
| |
| verify(windowManager).addView(any(), any()) |
| verify(windowManager, never()).removeView(any()) |
| } |
| |
| @Test |
| fun testHidesOnce() = testWithDisplay { |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| executor.runAllReady() |
| |
| repeat(5) { |
| overlayController.hide(SENSOR_ID) |
| executor.runAllReady() |
| } |
| |
| verify(windowManager).addView(any(), any()) |
| verify(windowManager).removeView(any()) |
| } |
| |
| @Test |
| fun testIgnoredForKeyguard() = testWithDisplay { |
| testIgnoredFor(REASON_AUTH_KEYGUARD) |
| } |
| |
| @Test |
| fun testShowsForMostSettings() = testWithDisplay { |
| `when`(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpEnrollTask())) |
| testIgnoredFor(REASON_AUTH_SETTINGS, ignored = false) |
| } |
| |
| @Test |
| fun testIgnoredForVerySpecificSettings() = testWithDisplay { |
| `when`(activityTaskManager.getTasks(anyInt())).thenReturn(listOf(fpSettingsTask())) |
| testIgnoredFor(REASON_AUTH_SETTINGS) |
| } |
| |
| private fun testIgnoredFor(reason: Int, ignored: Boolean = true) { |
| overlayController.show(SENSOR_ID, reason) |
| executor.runAllReady() |
| |
| verify(windowManager, if (ignored) never() else times(1)).addView(any(), any()) |
| } |
| |
| @Test |
| fun showsWithTaskbar() = testWithDisplay( |
| deviceConfig = DeviceConfig.X_ALIGNED, |
| { rotation = Surface.ROTATION_0 } |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbarOnY() = testWithDisplay( |
| deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, |
| { rotation = Surface.ROTATION_0 } |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbar90() = testWithDisplay( |
| deviceConfig = DeviceConfig.X_ALIGNED, |
| { rotation = Surface.ROTATION_90 } |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbar90OnY() = testWithDisplay( |
| deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, |
| { rotation = Surface.ROTATION_90 } |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbar180() = testWithDisplay( |
| deviceConfig = DeviceConfig.X_ALIGNED, |
| { rotation = Surface.ROTATION_180 } |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbar270OnY() = testWithDisplay( |
| deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, |
| { rotation = Surface.ROTATION_270 } |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbarCollapsedDown() = testWithDisplay( |
| deviceConfig = DeviceConfig.X_ALIGNED, |
| { rotation = Surface.ROTATION_270 }, |
| windowInsets = insetsForSmallNavbar() |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun showsWithTaskbarCollapsedDownOnY() = testWithDisplay( |
| deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, |
| { rotation = Surface.ROTATION_180 }, |
| windowInsets = insetsForSmallNavbar() |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| @Test |
| fun hidesWithTaskbarDown() = testWithDisplay( |
| deviceConfig = DeviceConfig.X_ALIGNED, |
| { rotation = Surface.ROTATION_180 }, |
| windowInsets = insetsForLargeNavbar() |
| ) { |
| hidesWithTaskbar(visible = false) |
| } |
| |
| @Test |
| fun hidesWithTaskbarDownOnY() = testWithDisplay( |
| deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED, |
| { rotation = Surface.ROTATION_270 }, |
| windowInsets = insetsForLargeNavbar() |
| ) { |
| hidesWithTaskbar(visible = true) |
| } |
| |
| private fun hidesWithTaskbar(visible: Boolean) { |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| executor.runAllReady() |
| |
| sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(visible, false) |
| executor.runAllReady() |
| |
| verify(windowManager).addView(any(), any()) |
| verify(windowManager, never()).removeView(any()) |
| verify(sidefpsView).visibility = if (visible) View.VISIBLE else View.GONE |
| } |
| |
| @Test |
| fun testIndicatorPlacementForXAlignedSensor() = testWithDisplay( |
| deviceConfig = DeviceConfig.X_ALIGNED |
| ) { |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| sideFpsController.overlayOffsets = sensorLocation |
| sideFpsController.updateOverlayParams( |
| windowManager.defaultDisplay, |
| indicatorBounds |
| ) |
| executor.runAllReady() |
| |
| verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) |
| |
| assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) |
| assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) |
| } |
| |
| @Test |
| fun testIndicatorPlacementForYAlignedSensor() = testWithDisplay( |
| deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED |
| ) { |
| sideFpsController.overlayOffsets = sensorLocation |
| sideFpsController.updateOverlayParams( |
| windowManager.defaultDisplay, |
| indicatorBounds |
| ) |
| overlayController.show(SENSOR_ID, REASON_UNKNOWN) |
| executor.runAllReady() |
| |
| verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) |
| assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth) |
| assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY) |
| } |
| } |
| |
| private fun insetsForSmallNavbar() = insetsWithBottom(60) |
| private fun insetsForLargeNavbar() = insetsWithBottom(100) |
| private fun insetsWithBottom(bottom: Int) = WindowInsets.Builder() |
| .setInsets(WindowInsets.Type.navigationBars(), Insets.of(0, 0, 0, bottom)) |
| .build() |
| |
| private fun fpEnrollTask() = settingsTask(".biometrics.fingerprint.FingerprintEnrollEnrolling") |
| private fun fpSettingsTask() = settingsTask(".biometrics.fingerprint.FingerprintSettings") |
| private fun settingsTask(cls: String) = ActivityManager.RunningTaskInfo().apply { |
| topActivity = ComponentName.createRelative("com.android.settings", cls) |
| } |