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 17 package com.android.systemui.biometrics 18 19 import android.annotation.SuppressLint 20 import android.annotation.UiThread 21 import android.content.Context 22 import android.graphics.PixelFormat 23 import android.graphics.Point 24 import android.graphics.Rect 25 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_BP 26 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD 27 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER 28 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS 29 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING 30 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR 31 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason 32 import android.hardware.fingerprint.FingerprintManager 33 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback 34 import android.os.Build 35 import android.os.RemoteException 36 import android.provider.Settings 37 import android.util.Log 38 import android.util.RotationUtils 39 import android.view.LayoutInflater 40 import android.view.MotionEvent 41 import android.view.Surface 42 import android.view.View 43 import android.view.WindowManager 44 import android.view.accessibility.AccessibilityManager 45 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener 46 import androidx.annotation.LayoutRes 47 import androidx.annotation.VisibleForTesting 48 import com.android.keyguard.KeyguardUpdateMonitor 49 import com.android.settingslib.udfps.UdfpsOverlayParams 50 import com.android.settingslib.udfps.UdfpsUtils 51 import com.android.systemui.R 52 import com.android.systemui.animation.ActivityLaunchAnimator 53 import com.android.systemui.biometrics.ui.controller.UdfpsKeyguardViewController 54 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor 55 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor 56 import com.android.systemui.dump.DumpManager 57 import com.android.systemui.flags.FeatureFlags 58 import com.android.systemui.flags.Flags 59 import com.android.systemui.flags.Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS 60 import com.android.systemui.keyguard.ui.adapter.UdfpsKeyguardViewControllerAdapter 61 import com.android.systemui.keyguard.ui.viewmodel.UdfpsKeyguardViewModels 62 import com.android.systemui.plugins.statusbar.StatusBarStateController 63 import com.android.systemui.shade.ShadeExpansionStateManager 64 import com.android.systemui.statusbar.LockscreenShadeTransitionController 65 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager 66 import com.android.systemui.statusbar.phone.SystemUIDialogManager 67 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController 68 import com.android.systemui.statusbar.policy.ConfigurationController 69 import com.android.systemui.statusbar.policy.KeyguardStateController 70 import com.android.systemui.util.settings.SecureSettings 71 import kotlinx.coroutines.ExperimentalCoroutinesApi 72 import javax.inject.Provider 73 74 private const val TAG = "UdfpsControllerOverlay" 75 76 @VisibleForTesting 77 const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui" 78 79 /** 80 * Keeps track of the overlay state and UI resources associated with a single FingerprintService 81 * request. This state can persist across configuration changes via the [show] and [hide] 82 * methods. 83 */ 84 @ExperimentalCoroutinesApi 85 @UiThread 86 class UdfpsControllerOverlay @JvmOverloads constructor( 87 private val context: Context, 88 fingerprintManager: FingerprintManager, 89 private val inflater: LayoutInflater, 90 private val windowManager: WindowManager, 91 private val accessibilityManager: AccessibilityManager, 92 private val statusBarStateController: StatusBarStateController, 93 private val shadeExpansionStateManager: ShadeExpansionStateManager, 94 private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, 95 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 96 private val dialogManager: SystemUIDialogManager, 97 private val dumpManager: DumpManager, 98 private val transitionController: LockscreenShadeTransitionController, 99 private val configurationController: ConfigurationController, 100 private val keyguardStateController: KeyguardStateController, 101 private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController, 102 private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider, 103 private val secureSettings: SecureSettings, 104 val requestId: Long, 105 @ShowReason val requestReason: Int, 106 private val controllerCallback: IUdfpsOverlayControllerCallback, 107 private val onTouch: (View, MotionEvent, Boolean) -> Boolean, 108 private val activityLaunchAnimator: ActivityLaunchAnimator, 109 private val featureFlags: FeatureFlags, 110 private val primaryBouncerInteractor: PrimaryBouncerInteractor, 111 private val alternateBouncerInteractor: AlternateBouncerInteractor, 112 private val isDebuggable: Boolean = Build.IS_DEBUGGABLE, 113 private val udfpsUtils: UdfpsUtils, 114 private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate, 115 private val udfpsKeyguardViewModels: Provider<UdfpsKeyguardViewModels>, 116 ) { 117 /** The view, when [isShowing], or null. */ 118 var overlayView: UdfpsView? = null 119 private set 120 121 private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams() 122 private var sensorBounds: Rect = Rect() 123 124 private var overlayTouchListener: TouchExplorationStateChangeListener? = null 125 126 private val coreLayoutParams = WindowManager.LayoutParams( 127 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 128 0 /* flags set in computeLayoutParams() */, 129 PixelFormat.TRANSLUCENT 130 ).apply { 131 title = TAG 132 fitInsetsTypes = 0 133 gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT 134 layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS 135 flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or 136 WindowManager.LayoutParams.FLAG_SPLIT_TOUCH) 137 privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY 138 // Avoid announcing window title. 139 accessibilityTitle = " " 140 141 if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { 142 inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY 143 } 144 } 145 146 /** If the overlay is currently showing. */ 147 val isShowing: Boolean 148 get() = overlayView != null 149 150 /** Opposite of [isShowing]. */ 151 val isHiding: Boolean 152 get() = overlayView == null 153 154 /** The animation controller if the overlay [isShowing]. */ 155 val animationViewController: UdfpsAnimationViewController<*>? 156 get() = overlayView?.animationViewController 157 158 private var touchExplorationEnabled = false 159 160 private fun shouldRemoveEnrollmentUi(): Boolean { 161 if (isDebuggable) { 162 return Settings.Global.getInt( 163 context.contentResolver, 164 SETTING_REMOVE_ENROLLMENT_UI, 165 0 /* def */ 166 ) != 0 167 } 168 return false 169 } 170 171 /** Show the overlay or return false and do nothing if it is already showing. */ 172 @SuppressLint("ClickableViewAccessibility") 173 fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean { 174 if (overlayView == null) { 175 overlayParams = params 176 sensorBounds = Rect(params.sensorBounds) 177 try { 178 overlayView = (inflater.inflate( 179 R.layout.udfps_view, null, false 180 ) as UdfpsView).apply { 181 overlayParams = params 182 setUdfpsDisplayModeProvider(udfpsDisplayModeProvider) 183 val animation = inflateUdfpsAnimation(this, controller) 184 if (animation != null) { 185 animation.init() 186 animationViewController = animation 187 } 188 // This view overlaps the sensor area 189 // prevent it from being selectable during a11y 190 if (requestReason.isImportantForAccessibility()) { 191 importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO 192 } 193 194 windowManager.addView(this, coreLayoutParams.updateDimensions(animation)) 195 sensorRect = sensorBounds 196 touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled 197 overlayTouchListener = TouchExplorationStateChangeListener { 198 if (accessibilityManager.isTouchExplorationEnabled) { 199 setOnHoverListener { v, event -> onTouch(v, event, true) } 200 setOnTouchListener(null) 201 touchExplorationEnabled = true 202 } else { 203 setOnHoverListener(null) 204 setOnTouchListener { v, event -> onTouch(v, event, true) } 205 touchExplorationEnabled = false 206 } 207 } 208 accessibilityManager.addTouchExplorationStateChangeListener( 209 overlayTouchListener!! 210 ) 211 overlayTouchListener?.onTouchExplorationStateChanged(true) 212 useExpandedOverlay = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) 213 } 214 } catch (e: RuntimeException) { 215 Log.e(TAG, "showUdfpsOverlay | failed to add window", e) 216 } 217 return true 218 } 219 220 Log.v(TAG, "showUdfpsOverlay | the overlay is already showing") 221 return false 222 } 223 224 fun inflateUdfpsAnimation( 225 view: UdfpsView, 226 controller: UdfpsController 227 ): UdfpsAnimationViewController<*>? { 228 val isEnrollment = when (requestReason) { 229 REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true 230 else -> false 231 } 232 233 val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) { 234 REASON_AUTH_OTHER 235 } else { 236 requestReason 237 } 238 239 return when (filteredRequestReason) { 240 REASON_ENROLL_FIND_SENSOR, 241 REASON_ENROLL_ENROLLING -> { 242 // Enroll udfps UI is handled by settings, so use empty view here 243 UdfpsFpmEmptyViewController( 244 view.addUdfpsView(R.layout.udfps_fpm_empty_view){ 245 updateAccessibilityViewLocation(sensorBounds) 246 }, 247 statusBarStateController, 248 shadeExpansionStateManager, 249 dialogManager, 250 dumpManager 251 ) 252 } 253 REASON_AUTH_KEYGUARD -> { 254 if (featureFlags.isEnabled(REFACTOR_UDFPS_KEYGUARD_VIEWS)) { 255 udfpsKeyguardViewModels.get().setSensorBounds(sensorBounds) 256 UdfpsKeyguardViewController( 257 view.addUdfpsView(R.layout.udfps_keyguard_view), 258 statusBarStateController, 259 shadeExpansionStateManager, 260 dialogManager, 261 dumpManager, 262 alternateBouncerInteractor, 263 udfpsKeyguardViewModels.get(), 264 ) 265 } else { 266 UdfpsKeyguardViewControllerLegacy( 267 view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) { 268 updateSensorLocation(sensorBounds) 269 }, 270 statusBarStateController, 271 shadeExpansionStateManager, 272 statusBarKeyguardViewManager, 273 keyguardUpdateMonitor, 274 dumpManager, 275 transitionController, 276 configurationController, 277 keyguardStateController, 278 unlockedScreenOffAnimationController, 279 dialogManager, 280 controller, 281 activityLaunchAnimator, 282 featureFlags, 283 primaryBouncerInteractor, 284 alternateBouncerInteractor, 285 udfpsKeyguardAccessibilityDelegate, 286 ) 287 } 288 } 289 REASON_AUTH_BP -> { 290 // note: empty controller, currently shows no visual affordance 291 UdfpsBpViewController( 292 view.addUdfpsView(R.layout.udfps_bp_view), 293 statusBarStateController, 294 shadeExpansionStateManager, 295 dialogManager, 296 dumpManager 297 ) 298 } 299 REASON_AUTH_OTHER, 300 REASON_AUTH_SETTINGS -> { 301 UdfpsFpmEmptyViewController( 302 view.addUdfpsView(R.layout.udfps_fpm_empty_view), 303 statusBarStateController, 304 shadeExpansionStateManager, 305 dialogManager, 306 dumpManager 307 ) 308 } 309 else -> { 310 Log.e(TAG, "Animation for reason $requestReason not supported yet") 311 null 312 } 313 } 314 } 315 316 /** Hide the overlay or return false and do nothing if it is already hidden. */ 317 fun hide(): Boolean { 318 val wasShowing = isShowing 319 320 overlayView?.apply { 321 if (isDisplayConfigured) { 322 unconfigureDisplay() 323 } 324 windowManager.removeView(this) 325 setOnTouchListener(null) 326 setOnHoverListener(null) 327 animationViewController = null 328 overlayTouchListener?.let { 329 accessibilityManager.removeTouchExplorationStateChangeListener(it) 330 } 331 } 332 overlayView = null 333 overlayTouchListener = null 334 335 return wasShowing 336 } 337 338 /** 339 * This function computes the angle of touch relative to the sensor and maps 340 * the angle to a list of help messages which are announced if accessibility is enabled. 341 * 342 */ 343 fun onTouchOutsideOfSensorArea(scaledTouch: Point) { 344 val theStr = 345 udfpsUtils.onTouchOutsideOfSensorArea( 346 touchExplorationEnabled, 347 context, 348 scaledTouch.x, 349 scaledTouch.y, 350 overlayParams 351 ) 352 if (theStr != null) { 353 animationViewController?.doAnnounceForAccessibility(theStr) 354 } 355 } 356 357 /** Cancel this request. */ 358 fun cancel() { 359 try { 360 controllerCallback.onUserCanceled() 361 } catch (e: RemoteException) { 362 Log.e(TAG, "Remote exception", e) 363 } 364 } 365 366 /** Checks if the id is relevant for this overlay. */ 367 fun matchesRequestId(id: Long): Boolean = requestId == -1L || requestId == id 368 369 private fun WindowManager.LayoutParams.updateDimensions( 370 animation: UdfpsAnimationViewController<*>? 371 ): WindowManager.LayoutParams { 372 val paddingX = animation?.paddingX ?: 0 373 val paddingY = animation?.paddingY ?: 0 374 if (!featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION) && animation != null && 375 animation.listenForTouchesOutsideView()) { 376 flags = flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 377 } 378 379 val isEnrollment = when (requestReason) { 380 REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true 381 else -> false 382 } 383 384 // Use expanded overlay unless touchExploration enabled 385 var rotatedBounds = 386 if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { 387 if (accessibilityManager.isTouchExplorationEnabled && isEnrollment) { 388 Rect(overlayParams.sensorBounds) 389 } else { 390 Rect( 391 0, 392 0, 393 overlayParams.naturalDisplayWidth, 394 overlayParams.naturalDisplayHeight 395 ) 396 } 397 } else { 398 Rect(overlayParams.sensorBounds) 399 } 400 401 val rot = overlayParams.rotation 402 if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { 403 if (!shouldRotate(animation)) { 404 Log.v( 405 TAG, "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) + 406 " animation=$animation" + 407 " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" + 408 " isOccluded=${keyguardStateController.isOccluded}" 409 ) 410 } else { 411 Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot)) 412 RotationUtils.rotateBounds( 413 rotatedBounds, 414 overlayParams.naturalDisplayWidth, 415 overlayParams.naturalDisplayHeight, 416 rot 417 ) 418 419 if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) { 420 RotationUtils.rotateBounds( 421 sensorBounds, 422 overlayParams.naturalDisplayWidth, 423 overlayParams.naturalDisplayHeight, 424 rot 425 ) 426 } 427 } 428 } 429 430 x = rotatedBounds.left - paddingX 431 y = rotatedBounds.top - paddingY 432 height = rotatedBounds.height() + 2 * paddingX 433 width = rotatedBounds.width() + 2 * paddingY 434 435 return this 436 } 437 438 private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean { 439 if (animation !is UdfpsKeyguardViewControllerAdapter) { 440 // always rotate view if we're not on the keyguard 441 return true 442 } 443 444 // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded 445 return !(keyguardUpdateMonitor.isGoingToSleep || !keyguardStateController.isOccluded) 446 } 447 448 private inline fun <reified T : View> UdfpsView.addUdfpsView( 449 @LayoutRes id: Int, 450 init: T.() -> Unit = {} 451 ): T { 452 val subView = inflater.inflate(id, null) as T 453 addView(subView) 454 subView.init() 455 return subView 456 } 457 } 458 459 @ShowReason 460 private fun Int.isImportantForAccessibility() = 461 this == REASON_ENROLL_FIND_SENSOR || 462 this == REASON_ENROLL_ENROLLING || 463 this == REASON_AUTH_BP 464