blob: 7bb4708443e9321096aed7f58a800357688065bd [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.animation.AnimatorListenerAdapter
import android.app.ActivityTaskManager
import android.content.Context
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.hardware.biometrics.BiometricOverlayConstants
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
import android.hardware.display.DisplayManager
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
import android.hardware.fingerprint.ISidefpsController
import android.os.Handler
import android.util.Log
import android.view.Display
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
import com.airbnb.lottie.LottieProperty
import com.airbnb.lottie.model.KeyPath
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
private const val TAG = "SidefpsController"
/**
* Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events.
*/
@SysUISingleton
class SidefpsController @Inject constructor(
private val context: Context,
private val layoutInflater: LayoutInflater,
fingerprintManager: FingerprintManager?,
private val windowManager: WindowManager,
private val activityTaskManager: ActivityTaskManager,
overviewProxyService: OverviewProxyService,
displayManager: DisplayManager,
@Main mainExecutor: DelayableExecutor,
@Main private val handler: Handler
) {
@VisibleForTesting
val sensorProps: FingerprintSensorPropertiesInternal = fingerprintManager
?.sensorPropertiesInternal
?.firstOrNull { it.isAnySidefpsType }
?: throw IllegalStateException("no side fingerprint sensor")
@VisibleForTesting
val orientationListener = BiometricDisplayListener(
context,
displayManager,
handler,
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()
}
}
private val overlayViewParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_SYSTEM_ERROR,
Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
PixelFormat.TRANSLUCENT
).apply {
title = TAG
fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
gravity = Gravity.TOP or Gravity.LEFT
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
}
init {
fingerprintManager?.setSidefpsController(object : ISidefpsController.Stub() {
override fun show(
sensorId: Int,
@BiometricOverlayConstants.ShowReason reason: Int
) = if (reason.isReasonToShow(activityTaskManager)) doShow() else hide(sensorId)
private fun doShow() = mainExecutor.execute {
if (overlayView == null) {
overlayView = createOverlayForDisplay()
} else {
Log.v(TAG, "overlay already shown")
}
}
override fun hide(sensorId: Int) = mainExecutor.execute { overlayView = null }
})
overviewProxyService.addCallback(overviewProxyListener)
}
private fun onOrientationChanged() {
if (overlayView != null) {
overlayView = createOverlayForDisplay()
}
}
private fun createOverlayForDisplay(): View {
val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
val display = context.display!!
val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView
lottie.setAnimation(display.asSideFpsAnimation())
view.rotation = display.asSideFpsAnimationRotation()
updateOverlayParams(display, lottie.composition?.bounds ?: Rect())
lottie.addLottieOnCompositionLoadedListener {
if (overlayView == view) {
updateOverlayParams(display, it.bounds)
windowManager.updateViewLayout(overlayView, overlayViewParams)
}
}
lottie.addOverlayDynamicColor(context)
return view
}
private fun updateOverlayParams(display: Display, bounds: Rect) {
val isPortrait = display.isPortrait()
val size = windowManager.maximumWindowMetrics.bounds
val displayWidth = if (isPortrait) size.width() else size.height()
val displayHeight = if (isPortrait) size.height() else size.width()
val offsets = sensorProps.getLocation(display.uniqueId).let { location ->
if (location == null) {
Log.w(TAG, "No location specified for display: ${display.uniqueId}")
}
location ?: sensorProps.location
}
// ignore sensorLocationX and sensorRadius since it's assumed to be on the side
// of the device and centered at sensorLocationY
val (x, y) = when (display.rotation) {
Surface.ROTATION_90 ->
Pair(offsets.sensorLocationY, 0)
Surface.ROTATION_270 ->
Pair(displayHeight - offsets.sensorLocationY - bounds.width(), displayWidth)
Surface.ROTATION_180 ->
Pair(0, displayHeight - offsets.sensorLocationY - bounds.height())
else ->
Pair(displayWidth, offsets.sensorLocationY)
}
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
private fun Int.isReasonToShow(activityTaskManager: ActivityTaskManager): Boolean = when (this) {
REASON_AUTH_KEYGUARD -> false
REASON_AUTH_SETTINGS -> when (activityTaskManager.topClass()) {
// TODO(b/186176653): exclude fingerprint overlays from this list view
"com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false
else -> true
}
else -> true
}
private fun ActivityTaskManager.topClass(): String =
getTasks(1).firstOrNull()?.topActivity?.className ?: ""
@RawRes
private fun Display.asSideFpsAnimation(): Int = when (rotation) {
Surface.ROTATION_0 -> R.raw.sfps_pulse
Surface.ROTATION_180 -> R.raw.sfps_pulse
else -> R.raw.sfps_pulse_landscape
}
private fun Display.asSideFpsAnimationRotation(): Float = when (rotation) {
Surface.ROTATION_180 -> 180f
Surface.ROTATION_270 -> 180f
else -> 0f
}
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)
for (key in listOf(".blue600", ".blue400")) {
addValueCallback(
KeyPath(key, "**"),
LottieProperty.COLOR_FILTER
) { PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP) }
}
}
if (composition != null) {
update()
} else {
addLottieOnCompositionLoadedListener { update() }
}
}