Ensure side fingerprint sensor overlay shows above all other content.

When the overlay occludes the navigation bar it is hidden after a short delay.

Also consoldiates common overlay flags and removes an unused method.

Fix: 201538654
Test: manual (rotate device and enroll with navbar aligned with power button)
Change-Id: I7a707c1557decc9585a04594ea2856c507ae7f28
This commit is contained in:
Joe Bolinger
2021-09-29 17:08:54 -07:00
parent a208bcce58
commit 8e9db7ebbb
4 changed files with 174 additions and 46 deletions

View File

@@ -15,6 +15,8 @@
*/
package com.android.systemui.biometrics
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.PorterDuff
@@ -33,6 +35,8 @@ import android.view.Gravity
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 androidx.annotation.RawRes
import com.airbnb.lottie.LottieAnimationView
@@ -42,6 +46,7 @@ import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject
@@ -56,9 +61,10 @@ class SidefpsController @Inject constructor(
private val layoutInflater: LayoutInflater,
fingerprintManager: FingerprintManager?,
private val windowManager: WindowManager,
@Main mainExecutor: DelayableExecutor,
overviewProxyService: OverviewProxyService,
displayManager: DisplayManager,
@Main handler: Handler
@Main mainExecutor: DelayableExecutor,
@Main private val handler: Handler
) {
@VisibleForTesting
val sensorProps: FingerprintSensorPropertiesInternal = fingerprintManager
@@ -74,15 +80,33 @@ class SidefpsController @Inject constructor(
BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
) { onOrientationChanged() }
@VisibleForTesting
val overviewProxyListener = object : OverviewProxyService.OverviewProxyListener {
override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
overlayView?.let { view ->
handler.postDelayed({ updateOverlayVisibility(view) }, 500)
}
}
}
private val animationDuration =
context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
private var overlayHideAnimator: ViewPropertyAnimator? = null
private var overlayView: View? = null
set(value) {
field?.let { oldView ->
windowManager.removeView(oldView)
orientationListener.disable()
}
overlayHideAnimator?.cancel()
overlayHideAnimator = null
field = value
field?.let { newView ->
windowManager.addView(newView, overlayViewParams)
updateOverlayVisibility(newView)
orientationListener.enable()
}
}
@@ -90,11 +114,8 @@ class SidefpsController @Inject constructor(
private val overlayViewParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY,
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
PixelFormat.TRANSLUCENT
).apply {
title = TAG
@@ -121,6 +142,7 @@ class SidefpsController @Inject constructor(
override fun hide(sensorId: Int) = mainExecutor.execute { overlayView = null }
})
overviewProxyService.addCallback(overviewProxyListener)
}
private fun onOrientationChanged() {
@@ -176,6 +198,33 @@ class SidefpsController @Inject constructor(
overlayViewParams.x = x
overlayViewParams.y = y
}
private fun updateOverlayVisibility(view: View) {
if (view != overlayView) {
return
}
// hide after a few seconds if the sensor is oriented down and there are
// large overlapping system bars
if ((context.display?.rotation == Surface.ROTATION_270) &&
windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar()) {
overlayHideAnimator = view.animate()
.alpha(0f)
.setStartDelay(3_000)
.setDuration(animationDuration)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
view.visibility = View.GONE
overlayHideAnimator = null
}
})
} else {
overlayHideAnimator?.cancel()
overlayHideAnimator = null
view.alpha = 1f
view.visibility = View.VISIBLE
}
}
}
@BiometricOverlayConstants.ShowReason
@@ -200,6 +249,9 @@ private fun Display.asSideFpsAnimationRotation(): Float = when (rotation) {
private fun Display.isPortrait(): Boolean =
rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
private fun WindowInsets.hasBigNavigationBar(): Boolean =
getInsets(WindowInsets.Type.navigationBars()).bottom >= 70
private fun LottieAnimationView.addOverlayDynamicColor(context: Context) {
fun update() {
val c = context.getColor(R.color.biometric_dialog_accent)

View File

@@ -583,7 +583,7 @@ public class UdfpsController implements DozeReceiver {
mCoreLayoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
getCoreLayoutParamFlags(),
Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
PixelFormat.TRANSLUCENT);
mCoreLayoutParams.setTitle(TAG);
mCoreLayoutParams.setFitInsetsTypes(0);
@@ -616,13 +616,6 @@ public class UdfpsController implements DozeReceiver {
}
}
private int getCoreLayoutParamFlags() {
return WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
@Nullable
private FingerprintSensorPropertiesInternal findFirstUdfps() {
for (FingerprintSensorPropertiesInternal props :
@@ -685,7 +678,7 @@ public class UdfpsController implements DozeReceiver {
final int paddingX = animation != null ? animation.getPaddingX() : 0;
final int paddingY = animation != null ? animation.getPaddingY() : 0;
mCoreLayoutParams.flags = getCoreLayoutParamFlags();
mCoreLayoutParams.flags = Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS;
if (animation != null && animation.listenForTouchesOutsideView()) {
mCoreLayoutParams.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
}

View File

@@ -31,6 +31,7 @@ import android.hardware.biometrics.SensorPropertiesInternal;
import android.os.UserManager;
import android.util.DisplayMetrics;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -46,6 +47,13 @@ public class Utils {
public static final int CREDENTIAL_PATTERN = 2;
public static final int CREDENTIAL_PASSWORD = 3;
/** Base set of layout flags for fingerprint overlay widgets. */
public static final int FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS =
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
@Retention(RetentionPolicy.SOURCE)
@IntDef({CREDENTIAL_PIN, CREDENTIAL_PATTERN, CREDENTIAL_PASSWORD})
@interface CredentialType {}
@@ -55,11 +63,6 @@ public class Utils {
/ DisplayMetrics.DENSITY_DEFAULT);
}
static float pixelsToDp(Context context, float pixels) {
return pixels / ((float) context.getResources().getDisplayMetrics().densityDpi
/ DisplayMetrics.DENSITY_DEFAULT);
}
static void notifyAccessibilityContentChanged(AccessibilityManager am, ViewGroup view) {
if (!am.isEnabled()) {
return;

View File

@@ -16,6 +16,8 @@
package com.android.systemui.biometrics
import android.animation.Animator
import android.graphics.Insets
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_UNKNOWN
@@ -33,7 +35,9 @@ 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
@@ -41,6 +45,7 @@ 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 org.junit.Before
@@ -53,6 +58,8 @@ 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.anyLong
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
@@ -81,6 +88,8 @@ class SidefpsControllerTest : SysuiTestCase() {
@Mock
lateinit var displayManager: DisplayManager
@Mock
lateinit var overviewProxyService: OverviewProxyService
@Mock
lateinit var handler: Handler
@Captor
lateinit var overlayCaptor: ArgumentCaptor<View>
@@ -91,45 +100,62 @@ class SidefpsControllerTest : SysuiTestCase() {
@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
}
}
`when`(fingerprintManager.sensorPropertiesInternal).thenReturn(
listOf(
FingerprintSensorPropertiesInternal(
SENSOR_ID,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
listOf() /* componentInfo */,
FingerprintSensorProperties.TYPE_POWER_BUTTON,
true /* resetLockoutRequiresHardwareAuthToken */
)
)
)
`when`(windowManager.defaultDisplay).thenReturn(
Display(
DisplayManagerGlobal.getInstance(),
DISPLAY_ID,
DisplayInfo(),
DEFAULT_DISPLAY_ADJUSTMENTS
listOf(
FingerprintSensorPropertiesInternal(
SENSOR_ID,
SensorProperties.STRENGTH_STRONG,
5 /* maxEnrollmentsPerUser */,
listOf() /* componentInfo */,
FingerprintSensorProperties.TYPE_POWER_BUTTON,
true /* resetLockoutRequiresHardwareAuthToken */
)
)
)
`when`(windowManager.maximumWindowMetrics).thenReturn(
WindowMetrics(Rect(0, 0, 800, 800), WindowInsets.CONSUMED)
)
}
private fun testWithDisplay(initInfo: DisplayInfo.() -> Unit = {}, block: () -> Unit) {
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)
sideFpsController = SidefpsController(
mContext, layoutInflater, fingerprintManager, windowManager, executor,
displayManager, handler
context.createDisplayContext(display), layoutInflater, fingerprintManager,
windowManager, overviewProxyService, displayManager, executor, handler
)
overlayController = ArgumentCaptor.forClass(ISidefpsController::class.java).apply {
verify(fingerprintManager).setSidefpsController(capture())
}.value
block()
}
@Test
fun testSubscribesToOrientationChangesWhenShowingOverlay() {
fun testSubscribesToOrientationChangesWhenShowingOverlay() = testWithDisplay {
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
@@ -141,7 +167,7 @@ class SidefpsControllerTest : SysuiTestCase() {
}
@Test
fun testShowsAndHides() {
fun testShowsAndHides() = testWithDisplay {
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
@@ -156,7 +182,7 @@ class SidefpsControllerTest : SysuiTestCase() {
}
@Test
fun testShowsOnce() {
fun testShowsOnce() = testWithDisplay {
repeat(5) {
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
@@ -167,7 +193,7 @@ class SidefpsControllerTest : SysuiTestCase() {
}
@Test
fun testHidesOnce() {
fun testHidesOnce() = testWithDisplay {
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
@@ -181,10 +207,64 @@ class SidefpsControllerTest : SysuiTestCase() {
}
@Test
fun testIgnoredForKeyguard() {
overlayController.show(SENSOR_ID, REASON_AUTH_KEYGUARD)
fun testIgnoredForKeyguard() = testWithDisplay {
testIgnoredFor(REASON_AUTH_KEYGUARD)
}
private fun testIgnoredFor(reason: Int) {
overlayController.show(SENSOR_ID, reason)
executor.runAllReady()
verify(windowManager, never()).addView(any(), any())
}
@Test
fun showsWithTaskbar() = testWithDisplay({ rotation = Surface.ROTATION_0 }) {
hidesWithTaskbar(visible = true)
}
@Test
fun showsWithTaskbar90() = testWithDisplay({ rotation = Surface.ROTATION_90 }) {
hidesWithTaskbar(visible = true)
}
@Test
fun showsWithTaskbar180() = testWithDisplay({ rotation = Surface.ROTATION_180 }) {
hidesWithTaskbar(visible = true)
}
@Test
fun showsWithTaskbarCollapsedDown() = testWithDisplay({ rotation = Surface.ROTATION_270 }) {
`when`(windowManager.currentWindowMetrics).thenReturn(
WindowMetrics(Rect(0, 0, 800, 800), insetsForSmallNavbar())
)
hidesWithTaskbar(visible = true)
}
@Test
fun hidesWithTaskbarDown() = testWithDisplay({ rotation = Surface.ROTATION_270 }) {
`when`(windowManager.currentWindowMetrics).thenReturn(
WindowMetrics(Rect(0, 0, 800, 800), insetsForLargeNavbar())
)
hidesWithTaskbar(visible = false)
}
private fun hidesWithTaskbar(visible: Boolean) {
overlayController.show(SENSOR_ID, REASON_UNKNOWN)
executor.runAllReady()
sideFpsController.overviewProxyListener.onTaskbarStatusUpdated(true, false)
executor.runAllReady()
verify(windowManager).addView(any(), any())
verify(windowManager, never()).removeView(any())
verify(sidefpsView).visibility = if (visible) View.VISIBLE else View.GONE
}
}
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()