• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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