1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.biometrics 17 18 import android.animation.Animator 19 import android.animation.AnimatorListenerAdapter 20 import android.app.ActivityTaskManager 21 import android.content.Context 22 import android.graphics.PixelFormat 23 import android.graphics.PorterDuff 24 import android.graphics.PorterDuffColorFilter 25 import android.graphics.Rect 26 import android.hardware.biometrics.BiometricOverlayConstants 27 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD 28 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS 29 import android.hardware.display.DisplayManager 30 import android.hardware.fingerprint.FingerprintManager 31 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal 32 import android.hardware.fingerprint.ISidefpsController 33 import android.os.Handler 34 import android.util.Log 35 import android.view.Display 36 import android.view.Gravity 37 import android.view.LayoutInflater 38 import android.view.Surface 39 import android.view.View 40 import android.view.ViewPropertyAnimator 41 import android.view.WindowInsets 42 import android.view.WindowManager 43 import androidx.annotation.RawRes 44 import com.airbnb.lottie.LottieAnimationView 45 import com.airbnb.lottie.LottieProperty 46 import com.airbnb.lottie.model.KeyPath 47 import com.android.internal.annotations.VisibleForTesting 48 import com.android.systemui.R 49 import com.android.systemui.dagger.SysUISingleton 50 import com.android.systemui.dagger.qualifiers.Main 51 import com.android.systemui.recents.OverviewProxyService 52 import com.android.systemui.util.concurrency.DelayableExecutor 53 import javax.inject.Inject 54 55 private const val TAG = "SidefpsController" 56 57 /** 58 * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events. 59 */ 60 @SysUISingleton 61 class SidefpsController @Inject constructor( 62 private val context: Context, 63 private val layoutInflater: LayoutInflater, 64 fingerprintManager: FingerprintManager?, 65 private val windowManager: WindowManager, 66 private val activityTaskManager: ActivityTaskManager, 67 overviewProxyService: OverviewProxyService, 68 displayManager: DisplayManager, 69 @Main mainExecutor: DelayableExecutor, 70 @Main private val handler: Handler 71 ) { 72 @VisibleForTesting 73 val sensorProps: FingerprintSensorPropertiesInternal = fingerprintManager 74 ?.sensorPropertiesInternal 75 ?.firstOrNull { it.isAnySidefpsType } 76 ?: throw IllegalStateException("no side fingerprint sensor") 77 78 @VisibleForTesting 79 val orientationListener = BiometricDisplayListener( 80 context, 81 displayManager, 82 handler, 83 BiometricDisplayListener.SensorType.SideFingerprint(sensorProps) 84 ) { onOrientationChanged() } 85 86 @VisibleForTesting 87 val overviewProxyListener = object : OverviewProxyService.OverviewProxyListener { 88 override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { 89 overlayView?.let { view -> 90 handler.postDelayed({ updateOverlayVisibility(view) }, 500) 91 } 92 } 93 } 94 95 private val animationDuration = 96 context.resources.getInteger(android.R.integer.config_mediumAnimTime).toLong() 97 98 private var overlayHideAnimator: ViewPropertyAnimator? = null 99 100 private var overlayView: View? = null 101 set(value) { 102 field?.let { oldView -> 103 windowManager.removeView(oldView) 104 orientationListener.disable() 105 } 106 overlayHideAnimator?.cancel() 107 overlayHideAnimator = null 108 109 field = value 110 field?.let { newView -> 111 windowManager.addView(newView, overlayViewParams) 112 updateOverlayVisibility(newView) 113 orientationListener.enable() 114 } 115 } 116 117 private val overlayViewParams = WindowManager.LayoutParams( 118 WindowManager.LayoutParams.WRAP_CONTENT, 119 WindowManager.LayoutParams.WRAP_CONTENT, 120 WindowManager.LayoutParams.TYPE_SYSTEM_ERROR, 121 Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS, 122 PixelFormat.TRANSLUCENT 123 ).apply { 124 title = TAG 125 fitInsetsTypes = 0 // overrides default, avoiding status bars during layout 126 gravity = Gravity.TOP or Gravity.LEFT 127 layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 128 privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY 129 } 130 131 init { 132 fingerprintManager?.setSidefpsController(object : ISidefpsController.Stub() { 133 override fun show( 134 sensorId: Int, 135 @BiometricOverlayConstants.ShowReason reason: Int 136 ) = if (reason.isReasonToShow(activityTaskManager)) doShow() else hide(sensorId) 137 138 private fun doShow() = mainExecutor.execute { 139 if (overlayView == null) { 140 overlayView = createOverlayForDisplay() 141 } else { 142 Log.v(TAG, "overlay already shown") 143 } 144 } 145 146 override fun hide(sensorId: Int) = mainExecutor.execute { overlayView = null } 147 }) 148 overviewProxyService.addCallback(overviewProxyListener) 149 } 150 151 private fun onOrientationChanged() { 152 if (overlayView != null) { 153 overlayView = createOverlayForDisplay() 154 } 155 } 156 157 private fun createOverlayForDisplay(): View { 158 val view = layoutInflater.inflate(R.layout.sidefps_view, null, false) 159 val display = context.display!! 160 161 val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView 162 lottie.setAnimation(display.asSideFpsAnimation()) 163 view.rotation = display.asSideFpsAnimationRotation() 164 165 updateOverlayParams(display, lottie.composition?.bounds ?: Rect()) 166 lottie.addLottieOnCompositionLoadedListener { 167 if (overlayView == view) { 168 updateOverlayParams(display, it.bounds) 169 windowManager.updateViewLayout(overlayView, overlayViewParams) 170 } 171 } 172 lottie.addOverlayDynamicColor(context) 173 174 return view 175 } 176 177 private fun updateOverlayParams(display: Display, bounds: Rect) { 178 val isPortrait = display.isPortrait() 179 val size = windowManager.maximumWindowMetrics.bounds 180 val displayWidth = if (isPortrait) size.width() else size.height() 181 val displayHeight = if (isPortrait) size.height() else size.width() 182 val offsets = sensorProps.getLocation(display.uniqueId).let { location -> 183 if (location == null) { 184 Log.w(TAG, "No location specified for display: ${display.uniqueId}") 185 } 186 location ?: sensorProps.location 187 } 188 189 // ignore sensorLocationX and sensorRadius since it's assumed to be on the side 190 // of the device and centered at sensorLocationY 191 val (x, y) = when (display.rotation) { 192 Surface.ROTATION_90 -> 193 Pair(offsets.sensorLocationY, 0) 194 Surface.ROTATION_270 -> 195 Pair(displayHeight - offsets.sensorLocationY - bounds.width(), displayWidth) 196 Surface.ROTATION_180 -> 197 Pair(0, displayHeight - offsets.sensorLocationY - bounds.height()) 198 else -> 199 Pair(displayWidth, offsets.sensorLocationY) 200 } 201 overlayViewParams.x = x 202 overlayViewParams.y = y 203 } 204 205 private fun updateOverlayVisibility(view: View) { 206 if (view != overlayView) { 207 return 208 } 209 210 // hide after a few seconds if the sensor is oriented down and there are 211 // large overlapping system bars 212 if ((context.display?.rotation == Surface.ROTATION_270) && 213 windowManager.currentWindowMetrics.windowInsets.hasBigNavigationBar()) { 214 overlayHideAnimator = view.animate() 215 .alpha(0f) 216 .setStartDelay(3_000) 217 .setDuration(animationDuration) 218 .setListener(object : AnimatorListenerAdapter() { 219 override fun onAnimationEnd(animation: Animator) { 220 view.visibility = View.GONE 221 overlayHideAnimator = null 222 } 223 }) 224 } else { 225 overlayHideAnimator?.cancel() 226 overlayHideAnimator = null 227 view.alpha = 1f 228 view.visibility = View.VISIBLE 229 } 230 } 231 } 232 233 @BiometricOverlayConstants.ShowReason 234 private fun Int.isReasonToShow(activityTaskManager: ActivityTaskManager): Boolean = when (this) { 235 REASON_AUTH_KEYGUARD -> false 236 REASON_AUTH_SETTINGS -> when (activityTaskManager.topClass()) { 237 // TODO(b/186176653): exclude fingerprint overlays from this list view 238 "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false 239 else -> true 240 } 241 else -> true 242 } 243 244 private fun ActivityTaskManager.topClass(): String = 245 getTasks(1).firstOrNull()?.topActivity?.className ?: "" 246 247 @RawRes 248 private fun Display.asSideFpsAnimation(): Int = when (rotation) { 249 Surface.ROTATION_0 -> R.raw.sfps_pulse 250 Surface.ROTATION_180 -> R.raw.sfps_pulse 251 else -> R.raw.sfps_pulse_landscape 252 } 253 254 private fun Display.asSideFpsAnimationRotation(): Float = when (rotation) { 255 Surface.ROTATION_180 -> 180f 256 Surface.ROTATION_270 -> 180f 257 else -> 0f 258 } 259 260 private fun Display.isPortrait(): Boolean = 261 rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 262 263 private fun WindowInsets.hasBigNavigationBar(): Boolean = 264 getInsets(WindowInsets.Type.navigationBars()).bottom >= 70 265 266 private fun LottieAnimationView.addOverlayDynamicColor(context: Context) { 267 fun update() { 268 val c = context.getColor(R.color.biometric_dialog_accent) 269 for (key in listOf(".blue600", ".blue400")) { 270 addValueCallback( 271 KeyPath(key, "**"), 272 LottieProperty.COLOR_FILTER 273 ) { PorterDuffColorFilter(c, PorterDuff.Mode.SRC_ATOP) } 274 } 275 } 276 277 if (composition != null) { 278 update() 279 } else { 280 addLottieOnCompositionLoadedListener { update() } 281 } 282 } 283