blob: dec2b82ed88f62827ed536c536f11ba0baccd627 [file] [log] [blame]
/*
* 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)
}