Merge "Ensure side fingerprint sensor overlay shows above all other content." into sc-v2-dev
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user