1 /*
<lambda>null2 * 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.Rect
24 import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_BP
25 import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
26 import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_OTHER
27 import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_SETTINGS
28 import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
29 import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
30 import android.hardware.biometrics.BiometricRequestConstants.RequestReason
31 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
32 import android.os.Build
33 import android.os.RemoteException
34 import android.os.Trace
35 import android.provider.Settings
36 import android.util.Log
37 import android.util.RotationUtils
38 import android.view.LayoutInflater
39 import android.view.MotionEvent
40 import android.view.Surface
41 import android.view.View
42 import android.view.WindowManager
43 import android.view.accessibility.AccessibilityManager
44 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
45 import androidx.annotation.LayoutRes
46 import androidx.annotation.VisibleForTesting
47 import com.android.keyguard.KeyguardUpdateMonitor
48 import com.android.systemui.Flags.udfpsViewPerformance
49 import com.android.systemui.animation.ActivityTransitionAnimator
50 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
51 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
52 import com.android.systemui.biometrics.ui.binder.UdfpsTouchOverlayBinder
53 import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay
54 import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayViewModel
55 import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
56 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
57 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
58 import com.android.systemui.dagger.qualifiers.Application
59 import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
60 import com.android.systemui.dump.DumpManager
61 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
62 import com.android.systemui.keyguard.shared.model.KeyguardState
63 import com.android.systemui.plugins.statusbar.StatusBarStateController
64 import com.android.systemui.power.domain.interactor.PowerInteractor
65 import com.android.systemui.res.R
66 import com.android.systemui.shade.domain.interactor.ShadeInteractor
67 import com.android.systemui.statusbar.LockscreenShadeTransitionController
68 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
69 import com.android.systemui.statusbar.phone.SystemUIDialogManager
70 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
71 import com.android.systemui.statusbar.policy.ConfigurationController
72 import com.android.systemui.statusbar.policy.KeyguardStateController
73 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
74 import dagger.Lazy
75 import kotlinx.coroutines.CoroutineScope
76 import kotlinx.coroutines.ExperimentalCoroutinesApi
77 import kotlinx.coroutines.Job
78 import kotlinx.coroutines.flow.Flow
79 import kotlinx.coroutines.flow.filter
80 import kotlinx.coroutines.flow.map
81 import kotlinx.coroutines.launch
82
83 private const val TAG = "UdfpsControllerOverlay"
84
85 @VisibleForTesting
86 const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
87
88 /**
89 * Keeps track of the overlay state and UI resources associated with a single FingerprintService
90 * request. This state can persist across configuration changes via the [show] and [hide]
91 * methods.
92 */
93 @ExperimentalCoroutinesApi
94 @UiThread
95 class UdfpsControllerOverlay @JvmOverloads constructor(
96 private val context: Context,
97 private val inflater: LayoutInflater,
98 private val windowManager: WindowManager,
99 private val accessibilityManager: AccessibilityManager,
100 private val statusBarStateController: StatusBarStateController,
101 private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
102 private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
103 private val dialogManager: SystemUIDialogManager,
104 private val dumpManager: DumpManager,
105 private val transitionController: LockscreenShadeTransitionController,
106 private val configurationController: ConfigurationController,
107 private val keyguardStateController: KeyguardStateController,
108 private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
109 private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
110 val requestId: Long,
111 @RequestReason val requestReason: Int,
112 private val controllerCallback: IUdfpsOverlayControllerCallback,
113 private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
114 private val activityTransitionAnimator: ActivityTransitionAnimator,
115 private val primaryBouncerInteractor: PrimaryBouncerInteractor,
116 private val alternateBouncerInteractor: AlternateBouncerInteractor,
117 private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
118 private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
119 private val transitionInteractor: KeyguardTransitionInteractor,
120 private val selectedUserInteractor: SelectedUserInteractor,
121 private val deviceEntryUdfpsTouchOverlayViewModel:
122 Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
123 private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
124 private val shadeInteractor: ShadeInteractor,
125 private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
126 private val powerInteractor: PowerInteractor,
127 @Application private val scope: CoroutineScope,
128 ) {
129 private val currentStateUpdatedToOffAodOrDozing: Flow<Unit> =
130 transitionInteractor.currentKeyguardState
131 .filter {
132 it == KeyguardState.OFF ||
133 it == KeyguardState.AOD ||
134 it == KeyguardState.DOZING
135 }
136 .map { } // map to Unit
137 private var listenForCurrentKeyguardState: Job? = null
138 private var addViewRunnable: Runnable? = null
139 private var overlayViewLegacy: UdfpsView? = null
140 private set
141 private var overlayTouchView: UdfpsTouchOverlay? = null
142
143 /**
144 * Get the current UDFPS overlay touch view which is a different View depending on whether
145 * the DeviceEntryUdfpsRefactor flag is enabled or not.
146 * @return The view, when [isShowing], else null
147 */
148 fun getTouchOverlay(): View? {
149 return if (DeviceEntryUdfpsRefactor.isEnabled) {
150 overlayTouchView
151 } else {
152 overlayViewLegacy
153 }
154 }
155
156 private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
157 private var sensorBounds: Rect = Rect()
158
159 private var overlayTouchListener: TouchExplorationStateChangeListener? = null
160
161 private val coreLayoutParams = WindowManager.LayoutParams(
162 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
163 0 /* flags set in computeLayoutParams() */,
164 PixelFormat.TRANSLUCENT
165 ).apply {
166 title = TAG
167 fitInsetsTypes = 0
168 gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
169 layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
170 flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
171 WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
172 privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY or
173 WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION
174 // Avoid announcing window title.
175 accessibilityTitle = " "
176 inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
177 }
178
179 /** If the overlay is currently showing. */
180 val isShowing: Boolean
181 get() = getTouchOverlay() != null
182
183 /** Opposite of [isShowing]. */
184 val isHiding: Boolean
185 get() = getTouchOverlay() == null
186
187 /** The animation controller if the overlay [isShowing]. */
188 val animationViewController: UdfpsAnimationViewController<*>?
189 get() = overlayViewLegacy?.animationViewController
190
191 private var touchExplorationEnabled = false
192
193 private fun shouldRemoveEnrollmentUi(): Boolean {
194 if (isDebuggable) {
195 return Settings.Global.getInt(
196 context.contentResolver,
197 SETTING_REMOVE_ENROLLMENT_UI,
198 0 /* def */
199 ) != 0
200 }
201 return false
202 }
203
204 /** Show the overlay or return false and do nothing if it is already showing. */
205 @SuppressLint("ClickableViewAccessibility")
206 fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
207 if (getTouchOverlay() == null) {
208 overlayParams = params
209 sensorBounds = Rect(params.sensorBounds)
210 try {
211 if (DeviceEntryUdfpsRefactor.isEnabled) {
212 overlayTouchView = (inflater.inflate(
213 R.layout.udfps_touch_overlay, null, false
214 ) as UdfpsTouchOverlay).apply {
215 // This view overlaps the sensor area
216 // prevent it from being selectable during a11y
217 if (requestReason.isImportantForAccessibility()) {
218 importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
219 }
220
221 addViewNowOrLater(this, null)
222 when (requestReason) {
223 REASON_AUTH_KEYGUARD ->
224 UdfpsTouchOverlayBinder.bind(
225 view = this,
226 viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(),
227 udfpsOverlayInteractor = udfpsOverlayInteractor,
228 )
229 else ->
230 UdfpsTouchOverlayBinder.bind(
231 view = this,
232 viewModel = defaultUdfpsTouchOverlayViewModel.get(),
233 udfpsOverlayInteractor = udfpsOverlayInteractor,
234 )
235 }
236 }
237 } else {
238 overlayViewLegacy = (inflater.inflate(
239 R.layout.udfps_view, null, false
240 ) as UdfpsView).apply {
241 overlayParams = params
242 setUdfpsDisplayModeProvider(udfpsDisplayModeProvider)
243 val animation = inflateUdfpsAnimation(this, controller)
244 if (animation != null) {
245 animation.init()
246 animationViewController = animation
247 }
248 // This view overlaps the sensor area
249 // prevent it from being selectable during a11y
250 if (requestReason.isImportantForAccessibility()) {
251 importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
252 }
253
254 addViewNowOrLater(this, animation)
255 sensorRect = sensorBounds
256 }
257 }
258 getTouchOverlay()?.apply {
259 touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
260 overlayTouchListener = TouchExplorationStateChangeListener {
261 if (accessibilityManager.isTouchExplorationEnabled) {
262 setOnHoverListener { v, event -> onTouch(v, event, true) }
263 setOnTouchListener(null)
264 touchExplorationEnabled = true
265 } else {
266 setOnHoverListener(null)
267 setOnTouchListener { v, event -> onTouch(v, event, true) }
268 touchExplorationEnabled = false
269 }
270 }
271 accessibilityManager.addTouchExplorationStateChangeListener(
272 overlayTouchListener!!
273 )
274 overlayTouchListener?.onTouchExplorationStateChanged(true)
275 }
276 } catch (e: RuntimeException) {
277 Log.e(TAG, "showUdfpsOverlay | failed to add window", e)
278 }
279 return true
280 }
281
282 Log.v(TAG, "showUdfpsOverlay | the overlay is already showing")
283 return false
284 }
285
286 private fun addViewNowOrLater(view: View, animation: UdfpsAnimationViewController<*>?) {
287 if (udfpsViewPerformance()) {
288 addViewRunnable = kotlinx.coroutines.Runnable {
289 Trace.setCounter("UdfpsAddView", 1)
290 windowManager.addView(
291 view,
292 coreLayoutParams.updateDimensions(animation)
293 )
294 }
295 if (powerInteractor.detailedWakefulness.value.isAwake()) {
296 // Device is awake, so we add the view immediately.
297 addViewIfPending()
298 } else {
299 listenForCurrentKeyguardState?.cancel()
300 listenForCurrentKeyguardState = scope.launch {
301 currentStateUpdatedToOffAodOrDozing.collect {
302 addViewIfPending()
303 }
304 }
305 }
306 } else {
307 windowManager.addView(
308 view,
309 coreLayoutParams.updateDimensions(animation)
310 )
311 }
312 }
313
314 private fun addViewIfPending() {
315 addViewRunnable?.let {
316 listenForCurrentKeyguardState?.cancel()
317 it.run()
318 }
319 addViewRunnable = null
320 }
321
322 fun updateOverlayParams(updatedOverlayParams: UdfpsOverlayParams) {
323 DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()
324 overlayParams = updatedOverlayParams
325 sensorBounds = updatedOverlayParams.sensorBounds
326 getTouchOverlay()?.let {
327 if (addViewRunnable == null) {
328 // Only updateViewLayout if there's no pending view to add to WM.
329 // If there is a pending view, that means the view hasn't been added yet so there's
330 // no need to update any layouts. Instead the correct params will be used when the
331 // view is eventually added.
332 windowManager.updateViewLayout(it, coreLayoutParams.updateDimensions(null))
333 }
334 }
335 }
336
337 fun inflateUdfpsAnimation(
338 view: UdfpsView,
339 controller: UdfpsController
340 ): UdfpsAnimationViewController<*>? {
341 DeviceEntryUdfpsRefactor.assertInLegacyMode()
342
343 val isEnrollment = when (requestReason) {
344 REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
345 else -> false
346 }
347
348 val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
349 REASON_AUTH_OTHER
350 } else {
351 requestReason
352 }
353
354 return when (filteredRequestReason) {
355 REASON_ENROLL_FIND_SENSOR,
356 REASON_ENROLL_ENROLLING -> {
357 // Enroll udfps UI is handled by settings, so use empty view here
358 UdfpsFpmEmptyViewController(
359 view.addUdfpsView(R.layout.udfps_fpm_empty_view){
360 updateAccessibilityViewLocation(sensorBounds)
361 },
362 statusBarStateController,
363 shadeInteractor,
364 dialogManager,
365 dumpManager,
366 udfpsOverlayInteractor,
367 )
368 }
369 REASON_AUTH_KEYGUARD -> {
370 UdfpsKeyguardViewControllerLegacy(
371 view.addUdfpsView(R.layout.udfps_keyguard_view_legacy) {
372 updateSensorLocation(sensorBounds)
373 },
374 statusBarStateController,
375 statusBarKeyguardViewManager,
376 keyguardUpdateMonitor,
377 dumpManager,
378 transitionController,
379 configurationController,
380 keyguardStateController,
381 unlockedScreenOffAnimationController,
382 dialogManager,
383 controller,
384 activityTransitionAnimator,
385 primaryBouncerInteractor,
386 alternateBouncerInteractor,
387 udfpsKeyguardAccessibilityDelegate,
388 selectedUserInteractor,
389 transitionInteractor,
390 shadeInteractor,
391 udfpsOverlayInteractor,
392 )
393 }
394 REASON_AUTH_BP -> {
395 // note: empty controller, currently shows no visual affordance
396 UdfpsBpViewController(
397 view.addUdfpsView(R.layout.udfps_bp_view),
398 statusBarStateController,
399 shadeInteractor,
400 dialogManager,
401 dumpManager,
402 udfpsOverlayInteractor,
403 )
404 }
405 REASON_AUTH_OTHER,
406 REASON_AUTH_SETTINGS -> {
407 UdfpsFpmEmptyViewController(
408 view.addUdfpsView(R.layout.udfps_fpm_empty_view),
409 statusBarStateController,
410 shadeInteractor,
411 dialogManager,
412 dumpManager,
413 udfpsOverlayInteractor,
414 )
415 }
416 else -> {
417 Log.e(TAG, "Animation for reason $requestReason not supported yet")
418 null
419 }
420 }
421 }
422
423 /** Hide the overlay or return false and do nothing if it is already hidden. */
424 fun hide(): Boolean {
425 val wasShowing = isShowing
426
427 overlayViewLegacy?.apply {
428 if (isDisplayConfigured) {
429 unconfigureDisplay()
430 }
431 animationViewController = null
432 }
433 if (DeviceEntryUdfpsRefactor.isEnabled) {
434 udfpsDisplayModeProvider.disable(null)
435 }
436 getTouchOverlay()?.apply {
437 if (udfpsViewPerformance()) {
438 if (this.parent != null) {
439 windowManager.removeView(this)
440 }
441 Trace.setCounter("UdfpsAddView", 0)
442 } else {
443 windowManager.removeView(this)
444 }
445 setOnTouchListener(null)
446 setOnHoverListener(null)
447 overlayTouchListener?.let {
448 accessibilityManager.removeTouchExplorationStateChangeListener(it)
449 }
450 }
451
452 overlayViewLegacy = null
453 overlayTouchView = null
454 overlayTouchListener = null
455 listenForCurrentKeyguardState?.cancel()
456
457 return wasShowing
458 }
459
460 /** Cancel this request. */
461 fun cancel() {
462 try {
463 controllerCallback.onUserCanceled()
464 } catch (e: RemoteException) {
465 Log.e(TAG, "Remote exception", e)
466 }
467 }
468
469 /** Checks if the id is relevant for this overlay. */
470 fun matchesRequestId(id: Long): Boolean = requestId == -1L || requestId == id
471
472 private fun WindowManager.LayoutParams.updateDimensions(
473 animation: UdfpsAnimationViewController<*>?
474 ): WindowManager.LayoutParams {
475 val paddingX = animation?.paddingX ?: 0
476 val paddingY = animation?.paddingY ?: 0
477
478 val isEnrollment = when (requestReason) {
479 REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
480 else -> false
481 }
482
483 // Use expanded overlay unless touchExploration enabled
484 var rotatedBounds =
485 if (accessibilityManager.isTouchExplorationEnabled && isEnrollment) {
486 Rect(overlayParams.sensorBounds)
487 } else {
488 Rect(
489 0,
490 0,
491 overlayParams.naturalDisplayWidth,
492 overlayParams.naturalDisplayHeight
493 )
494 }
495
496 val rot = overlayParams.rotation
497 if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
498 if (!shouldRotate(animation)) {
499 Log.v(
500 TAG,
501 "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) +
502 " animation=$animation" +
503 " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" +
504 " isOccluded=${keyguardStateController.isOccluded}"
505 )
506 } else {
507 Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot))
508 RotationUtils.rotateBounds(
509 rotatedBounds,
510 overlayParams.naturalDisplayWidth,
511 overlayParams.naturalDisplayHeight,
512 rot
513 )
514
515 RotationUtils.rotateBounds(
516 sensorBounds,
517 overlayParams.naturalDisplayWidth,
518 overlayParams.naturalDisplayHeight,
519 rot
520 )
521 }
522 }
523
524 x = rotatedBounds.left - paddingX
525 y = rotatedBounds.top - paddingY
526 height = rotatedBounds.height() + 2 * paddingX
527 width = rotatedBounds.width() + 2 * paddingY
528
529 return this
530 }
531
532 private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean {
533 val keyguardNotShowing =
534 if (DeviceEntryUdfpsRefactor.isEnabled) {
535 !keyguardStateController.isShowing
536 } else {
537 animation !is UdfpsKeyguardViewControllerLegacy
538 }
539
540 if (keyguardNotShowing) {
541 // always rotate view if we're not on the keyguard
542 return true
543 }
544
545 // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded
546 return !(keyguardUpdateMonitor.isGoingToSleep || !keyguardStateController.isOccluded)
547 }
548
549 private inline fun <reified T : View> UdfpsView.addUdfpsView(
550 @LayoutRes id: Int,
551 init: T.() -> Unit = {}
552 ): T {
553 val subView = inflater.inflate(id, null) as T
554 addView(subView)
555 subView.init()
556 return subView
557 }
558 }
559
560 @RequestReason
isImportantForAccessibilitynull561 private fun Int.isImportantForAccessibility() =
562 this == REASON_ENROLL_FIND_SENSOR ||
563 this == REASON_ENROLL_ENROLLING ||
564 this == REASON_AUTH_BP
565