diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt index b7398d86c16e2..dacc169f85eeb 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index c5270aa3d2743..81a4d590aec6c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -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; } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java index 322584c4aa690..6989547dce526 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/Utils.java @@ -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; diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt index 5fee7fbf87058..c98a504b87848 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt @@ -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 @@ -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(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() \ No newline at end of file