/* * 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() } } }