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.app.ActivityTaskManager 19 import android.content.Context 20 import android.content.res.Configuration 21 import android.graphics.Color 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.biometrics.SensorLocationInternal 30 import android.hardware.display.DisplayManager 31 import android.hardware.fingerprint.FingerprintManager 32 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal 33 import android.hardware.fingerprint.ISidefpsController 34 import android.os.Handler 35 import android.util.Log 36 import android.util.RotationUtils 37 import android.view.Display 38 import android.view.DisplayInfo 39 import android.view.Gravity 40 import android.view.LayoutInflater 41 import android.view.Surface 42 import android.view.View 43 import android.view.View.AccessibilityDelegate 44 import android.view.ViewPropertyAnimator 45 import android.view.WindowManager 46 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION 47 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY 48 import android.view.accessibility.AccessibilityEvent 49 import androidx.annotation.RawRes 50 import com.airbnb.lottie.LottieAnimationView 51 import com.airbnb.lottie.LottieProperty 52 import com.airbnb.lottie.model.KeyPath 53 import com.android.app.animation.Interpolators 54 import com.android.internal.annotations.VisibleForTesting 55 import com.android.keyguard.KeyguardPINView 56 import com.android.systemui.Dumpable 57 import com.android.systemui.R 58 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor 59 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor 60 import com.android.systemui.dagger.SysUISingleton 61 import com.android.systemui.dagger.qualifiers.Application 62 import com.android.systemui.dagger.qualifiers.Main 63 import com.android.systemui.dump.DumpManager 64 import com.android.systemui.util.boundsOnScreen 65 import com.android.systemui.util.concurrency.DelayableExecutor 66 import com.android.systemui.util.traceSection 67 import java.io.PrintWriter 68 import javax.inject.Inject 69 import kotlinx.coroutines.CoroutineScope 70 import kotlinx.coroutines.launch 71 72 private const val TAG = "SideFpsController" 73 74 /** 75 * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events. 76 */ 77 @SysUISingleton 78 class SideFpsController 79 @Inject 80 constructor( 81 private val context: Context, 82 private val layoutInflater: LayoutInflater, 83 fingerprintManager: FingerprintManager?, 84 private val windowManager: WindowManager, 85 private val activityTaskManager: ActivityTaskManager, 86 displayManager: DisplayManager, 87 private val displayStateInteractor: DisplayStateInteractor, 88 @Main private val mainExecutor: DelayableExecutor, 89 @Main private val handler: Handler, 90 private val alternateBouncerInteractor: AlternateBouncerInteractor, 91 @Application private val scope: CoroutineScope, 92 dumpManager: DumpManager 93 ) : Dumpable { 94 private val requests: HashSet<SideFpsUiRequestSource> = HashSet() 95 96 @VisibleForTesting 97 val sensorProps: FingerprintSensorPropertiesInternal = 98 fingerprintManager?.sideFpsSensorProperties 99 ?: throw IllegalStateException("no side fingerprint sensor") 100 101 @VisibleForTesting 102 val orientationReasonListener = 103 OrientationReasonListener( 104 context, 105 displayManager, 106 handler, 107 sensorProps, 108 { reason -> onOrientationChanged(reason) }, 109 BiometricOverlayConstants.REASON_UNKNOWN 110 ) 111 112 @VisibleForTesting val orientationListener = orientationReasonListener.orientationListener 113 114 private val isReverseDefaultRotation = 115 context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) 116 117 private var overlayShowAnimator: ViewPropertyAnimator? = null 118 119 private var overlayView: View? = null 120 set(value) { 121 field?.let { oldView -> 122 val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView 123 lottie.pauseAnimation() 124 windowManager.removeView(oldView) 125 orientationListener.disable() 126 } 127 overlayShowAnimator?.cancel() 128 overlayShowAnimator = null 129 130 field = value 131 field?.let { newView -> 132 if (requests.contains(SideFpsUiRequestSource.PRIMARY_BOUNCER)) { 133 newView.alpha = 0f 134 overlayShowAnimator = 135 newView 136 .animate() 137 .alpha(1f) 138 .setDuration(KeyguardPINView.ANIMATION_DURATION) 139 .setInterpolator(Interpolators.ALPHA_IN) 140 } 141 windowManager.addView(newView, overlayViewParams) 142 orientationListener.enable() 143 overlayShowAnimator?.start() 144 } 145 } 146 @VisibleForTesting var overlayOffsets: SensorLocationInternal = SensorLocationInternal.DEFAULT 147 148 private val displayInfo = DisplayInfo() 149 150 private val overlayViewParams = 151 WindowManager.LayoutParams( 152 WindowManager.LayoutParams.WRAP_CONTENT, 153 WindowManager.LayoutParams.WRAP_CONTENT, 154 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 155 Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS, 156 PixelFormat.TRANSLUCENT 157 ) 158 .apply { 159 title = TAG 160 fitInsetsTypes = 0 // overrides default, avoiding status bars during layout 161 gravity = Gravity.TOP or Gravity.LEFT 162 layoutInDisplayCutoutMode = 163 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 164 privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION 165 } 166 167 init { 168 fingerprintManager?.setSidefpsController( 169 object : ISidefpsController.Stub() { 170 override fun show( 171 sensorId: Int, 172 @BiometricOverlayConstants.ShowReason reason: Int 173 ) = 174 if (reason.isReasonToAutoShow(activityTaskManager)) { 175 show(SideFpsUiRequestSource.AUTO_SHOW, reason) 176 } else { 177 hide(SideFpsUiRequestSource.AUTO_SHOW) 178 } 179 180 override fun hide(sensorId: Int) = hide(SideFpsUiRequestSource.AUTO_SHOW) 181 } 182 ) 183 listenForAlternateBouncerVisibility() 184 185 dumpManager.registerDumpable(this) 186 } 187 188 private fun listenForAlternateBouncerVisibility() { 189 alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, "SideFpsController") 190 scope.launch { 191 alternateBouncerInteractor.isVisible.collect { isVisible: Boolean -> 192 if (isVisible) { 193 show(SideFpsUiRequestSource.ALTERNATE_BOUNCER, REASON_AUTH_KEYGUARD) 194 } else { 195 hide(SideFpsUiRequestSource.ALTERNATE_BOUNCER) 196 } 197 } 198 } 199 } 200 201 /** Shows the side fps overlay if not already shown. */ 202 fun show( 203 request: SideFpsUiRequestSource, 204 @BiometricOverlayConstants.ShowReason reason: Int = BiometricOverlayConstants.REASON_UNKNOWN 205 ) { 206 if (!displayStateInteractor.isInRearDisplayMode.value) { 207 requests.add(request) 208 mainExecutor.execute { 209 if (overlayView == null) { 210 traceSection( 211 "SideFpsController#show(request=${request.name}, reason=$reason)" 212 ) { 213 createOverlayForDisplay(reason) 214 } 215 } else { 216 Log.v(TAG, "overlay already shown") 217 } 218 } 219 } 220 } 221 222 /** Hides the fps overlay if shown. */ 223 fun hide(request: SideFpsUiRequestSource) { 224 requests.remove(request) 225 mainExecutor.execute { 226 if (requests.isEmpty()) { 227 traceSection("SideFpsController#hide(${request.name})") { overlayView = null } 228 } 229 } 230 } 231 232 override fun dump(pw: PrintWriter, args: Array<out String>) { 233 pw.println("requests:") 234 for (requestSource in requests) { 235 pw.println(" $requestSource.name") 236 } 237 238 pw.println("overlayView:") 239 pw.println(" width=${overlayView?.width}") 240 pw.println(" height=${overlayView?.height}") 241 pw.println(" boundsOnScreen=${overlayView?.boundsOnScreen}") 242 243 pw.println("displayStateInteractor:") 244 pw.println(" isInRearDisplayMode=${displayStateInteractor?.isInRearDisplayMode?.value}") 245 246 pw.println("sensorProps:") 247 pw.println(" displayId=${displayInfo.uniqueId}") 248 pw.println(" sensorType=${sensorProps?.sensorType}") 249 pw.println(" location=${sensorProps?.getLocation(displayInfo.uniqueId)}") 250 251 pw.println("overlayOffsets=$overlayOffsets") 252 pw.println("isReverseDefaultRotation=$isReverseDefaultRotation") 253 pw.println("currentRotation=${displayInfo.rotation}") 254 } 255 256 private fun onOrientationChanged(@BiometricOverlayConstants.ShowReason reason: Int) { 257 if (overlayView != null) { 258 createOverlayForDisplay(reason) 259 } 260 } 261 262 private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) { 263 val view = layoutInflater.inflate(R.layout.sidefps_view, null, false) 264 overlayView = view 265 val display = context.display!! 266 // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation 267 display.getDisplayInfo(displayInfo) 268 val offsets = 269 sensorProps.getLocation(display.uniqueId).let { location -> 270 if (location == null) { 271 Log.w(TAG, "No location specified for display: ${display.uniqueId}") 272 } 273 location ?: sensorProps.location 274 } 275 overlayOffsets = offsets 276 277 val lottie = view.requireViewById(R.id.sidefps_animation) as LottieAnimationView 278 view.rotation = 279 display.asSideFpsAnimationRotation( 280 offsets.isYAligned(), 281 getRotationFromDefault(displayInfo.rotation) 282 ) 283 lottie.setAnimation( 284 display.asSideFpsAnimation( 285 offsets.isYAligned(), 286 getRotationFromDefault(displayInfo.rotation) 287 ) 288 ) 289 lottie.addLottieOnCompositionLoadedListener { 290 // Check that view is not stale, and that overlayView has not been hidden/removed 291 if (overlayView != null && overlayView == view) { 292 updateOverlayParams(display, it.bounds) 293 } 294 } 295 orientationReasonListener.reason = reason 296 lottie.addOverlayDynamicColor(context, reason) 297 298 /** 299 * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from 300 * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is 301 * in focus 302 */ 303 view.setAccessibilityDelegate( 304 object : AccessibilityDelegate() { 305 override fun dispatchPopulateAccessibilityEvent( 306 host: View, 307 event: AccessibilityEvent 308 ): Boolean { 309 return if ( 310 event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 311 ) { 312 true 313 } else { 314 super.dispatchPopulateAccessibilityEvent(host, event) 315 } 316 } 317 } 318 ) 319 } 320 321 @VisibleForTesting 322 fun updateOverlayParams(display: Display, bounds: Rect) { 323 val isNaturalOrientation = display.isNaturalOrientation() 324 val isDefaultOrientation = 325 if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation 326 val size = windowManager.maximumWindowMetrics.bounds 327 328 val displayWidth = if (isDefaultOrientation) size.width() else size.height() 329 val displayHeight = if (isDefaultOrientation) size.height() else size.width() 330 val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height() 331 val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width() 332 333 val sensorBounds = 334 if (overlayOffsets.isYAligned()) { 335 Rect( 336 displayWidth - boundsWidth, 337 overlayOffsets.sensorLocationY, 338 displayWidth, 339 overlayOffsets.sensorLocationY + boundsHeight 340 ) 341 } else { 342 Rect( 343 overlayOffsets.sensorLocationX, 344 0, 345 overlayOffsets.sensorLocationX + boundsWidth, 346 boundsHeight 347 ) 348 } 349 350 RotationUtils.rotateBounds( 351 sensorBounds, 352 Rect(0, 0, displayWidth, displayHeight), 353 getRotationFromDefault(display.rotation) 354 ) 355 356 overlayViewParams.x = sensorBounds.left 357 overlayViewParams.y = sensorBounds.top 358 359 windowManager.updateViewLayout(overlayView, overlayViewParams) 360 } 361 362 private fun getRotationFromDefault(rotation: Int): Int = 363 if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation 364 } 365 366 private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal? 367 get() = this?.sensorPropertiesInternal?.firstOrNull { it.isAnySidefpsType } 368 369 /** Returns [True] when the device has a side fingerprint sensor. */ 370 fun FingerprintManager?.hasSideFpsSensor(): Boolean = this?.sideFpsSensorProperties != null 371 372 @BiometricOverlayConstants.ShowReason 373 private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Boolean = 374 when (this) { 375 REASON_AUTH_KEYGUARD -> false 376 REASON_AUTH_SETTINGS -> 377 when (activityTaskManager.topClass()) { 378 // TODO(b/186176653): exclude fingerprint overlays from this list view 379 "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false 380 else -> true 381 } 382 else -> true 383 } 384 385 private fun ActivityTaskManager.topClass(): String = 386 getTasks(1).firstOrNull()?.topActivity?.className ?: "" 387 388 @RawRes 389 private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int = 390 when (rotationFromDefault) { 391 Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape 392 Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape 393 else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse 394 } 395 396 private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float = 397 when (rotationFromDefault) { 398 Surface.ROTATION_90 -> if (yAligned) 0f else 180f 399 Surface.ROTATION_180 -> 180f 400 Surface.ROTATION_270 -> if (yAligned) 180f else 0f 401 else -> 0f 402 } 403 404 private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0 405 406 private fun Display.isNaturalOrientation(): Boolean = 407 rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 408 409 private fun LottieAnimationView.addOverlayDynamicColor( 410 context: Context, 411 @BiometricOverlayConstants.ShowReason reason: Int 412 ) { 413 fun update() { 414 val isKeyguard = reason == REASON_AUTH_KEYGUARD 415 if (isKeyguard) { 416 val color = 417 com.android.settingslib.Utils.getColorAttrDefaultColor( 418 context, 419 com.android.internal.R.attr.materialColorPrimaryFixed 420 ) 421 val outerRimColor = 422 com.android.settingslib.Utils.getColorAttrDefaultColor( 423 context, 424 com.android.internal.R.attr.materialColorPrimaryFixedDim 425 ) 426 val chevronFill = 427 com.android.settingslib.Utils.getColorAttrDefaultColor( 428 context, 429 com.android.internal.R.attr.materialColorOnPrimaryFixed 430 ) 431 addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) { 432 PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) 433 } 434 addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) { 435 PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP) 436 } 437 addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { 438 PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP) 439 } 440 } else { 441 if (!isDarkMode(context)) { 442 addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { 443 PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) 444 } 445 } 446 for (key in listOf(".blue600", ".blue400")) { 447 addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { 448 PorterDuffColorFilter( 449 context.getColor(R.color.settingslib_color_blue400), 450 PorterDuff.Mode.SRC_ATOP 451 ) 452 } 453 } 454 } 455 } 456 457 if (composition != null) { 458 update() 459 } else { 460 addLottieOnCompositionLoadedListener { update() } 461 } 462 } 463 464 private fun isDarkMode(context: Context): Boolean { 465 val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK 466 return darkMode == Configuration.UI_MODE_NIGHT_YES 467 } 468 469 @VisibleForTesting 470 class OrientationReasonListener( 471 context: Context, 472 displayManager: DisplayManager, 473 handler: Handler, 474 sensorProps: FingerprintSensorPropertiesInternal, 475 onOrientationChanged: (reason: Int) -> Unit, 476 @BiometricOverlayConstants.ShowReason var reason: Int 477 ) { 478 val orientationListener = 479 BiometricDisplayListener( 480 context, 481 displayManager, 482 handler, 483 BiometricDisplayListener.SensorType.SideFingerprint(sensorProps) 484 ) { 485 onOrientationChanged(reason) 486 } 487 } 488 489 /** 490 * The source of a request to show the side fps visual indicator. This is distinct from 491 * [BiometricOverlayConstants] which corrresponds with the reason fingerprint authentication is 492 * requested. 493 */ 494 enum class SideFpsUiRequestSource { 495 /** see [isReasonToAutoShow] */ 496 AUTO_SHOW, 497 /** Pin, pattern or password bouncer */ 498 PRIMARY_BOUNCER, 499 ALTERNATE_BOUNCER 500 } 501