• 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.BiometricOverlayConstants.REASON_AUTH_BP
25 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
26 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_OTHER
27 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
28 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_ENROLLING
29 import android.hardware.biometrics.BiometricOverlayConstants.REASON_ENROLL_FIND_SENSOR
30 import android.hardware.biometrics.BiometricOverlayConstants.ShowReason
31 import android.hardware.fingerprint.FingerprintManager
32 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
33 import android.os.Build
34 import android.os.RemoteException
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.R
49 import com.android.systemui.animation.ActivityLaunchAnimator
50 import com.android.systemui.dump.DumpManager
51 import com.android.systemui.flags.FeatureFlags
52 import com.android.systemui.flags.Flags
53 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor
54 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
55 import com.android.systemui.plugins.statusbar.StatusBarStateController
56 import com.android.systemui.shade.ShadeExpansionStateManager
57 import com.android.systemui.statusbar.LockscreenShadeTransitionController
58 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
59 import com.android.systemui.statusbar.phone.SystemUIDialogManager
60 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
61 import com.android.systemui.statusbar.policy.ConfigurationController
62 import com.android.systemui.statusbar.policy.KeyguardStateController
63 import com.android.systemui.util.settings.SecureSettings
64 
65 private const val TAG = "UdfpsControllerOverlay"
66 
67 @VisibleForTesting
68 const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
69 
70 /**
71  * Keeps track of the overlay state and UI resources associated with a single FingerprintService
72  * request. This state can persist across configuration changes via the [show] and [hide]
73  * methods.
74  */
75 @UiThread
76 class UdfpsControllerOverlay @JvmOverloads constructor(
77         private val context: Context,
78         fingerprintManager: FingerprintManager,
79         private val inflater: LayoutInflater,
80         private val windowManager: WindowManager,
81         private val accessibilityManager: AccessibilityManager,
82         private val statusBarStateController: StatusBarStateController,
83         private val shadeExpansionStateManager: ShadeExpansionStateManager,
84         private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
85         private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
86         private val dialogManager: SystemUIDialogManager,
87         private val dumpManager: DumpManager,
88         private val transitionController: LockscreenShadeTransitionController,
89         private val configurationController: ConfigurationController,
90         private val keyguardStateController: KeyguardStateController,
91         private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
92         private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
93         private val secureSettings: SecureSettings,
94         val requestId: Long,
95         @ShowReason val requestReason: Int,
96         private val controllerCallback: IUdfpsOverlayControllerCallback,
97         private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
98         private val activityLaunchAnimator: ActivityLaunchAnimator,
99         private val featureFlags: FeatureFlags,
100         private val primaryBouncerInteractor: PrimaryBouncerInteractor,
101         private val alternateBouncerInteractor: AlternateBouncerInteractor,
102         private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
103 ) {
104     /** The view, when [isShowing], or null. */
105     var overlayView: UdfpsView? = null
106         private set
107 
108     private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
109     private var sensorBounds: Rect = Rect()
110 
111     private var overlayTouchListener: TouchExplorationStateChangeListener? = null
112 
113     private val coreLayoutParams = WindowManager.LayoutParams(
114         WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
115         0 /* flags set in computeLayoutParams() */,
116         PixelFormat.TRANSLUCENT
117     ).apply {
118         title = TAG
119         fitInsetsTypes = 0
120         gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
121         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
122         flags = (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
123                 WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
124         privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
125         // Avoid announcing window title.
126         accessibilityTitle = " "
127 
128         if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
129             inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
130         }
131     }
132 
133     /** A helper if the [requestReason] was due to enrollment. */
134     val enrollHelper: UdfpsEnrollHelper? =
135         if (requestReason.isEnrollmentReason() && !shouldRemoveEnrollmentUi()) {
136             UdfpsEnrollHelper(context, fingerprintManager, secureSettings, requestReason)
137         } else {
138             null
139         }
140 
141     /** If the overlay is currently showing. */
142     val isShowing: Boolean
143         get() = overlayView != null
144 
145     /** Opposite of [isShowing]. */
146     val isHiding: Boolean
147         get() = overlayView == null
148 
149     /** The animation controller if the overlay [isShowing]. */
150     val animationViewController: UdfpsAnimationViewController<*>?
151         get() = overlayView?.animationViewController
152 
153     private var touchExplorationEnabled = false
154 
155     private fun shouldRemoveEnrollmentUi(): Boolean {
156         if (isDebuggable) {
157             return Settings.Global.getInt(
158                 context.contentResolver,
159                 SETTING_REMOVE_ENROLLMENT_UI,
160                 0 /* def */
161             ) != 0
162         }
163         return false
164     }
165 
166     /** Show the overlay or return false and do nothing if it is already showing. */
167     @SuppressLint("ClickableViewAccessibility")
168     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
169         if (overlayView == null) {
170             overlayParams = params
171             sensorBounds = Rect(params.sensorBounds)
172             try {
173                 overlayView = (inflater.inflate(
174                     R.layout.udfps_view, null, false
175                 ) as UdfpsView).apply {
176                     overlayParams = params
177                     setUdfpsDisplayModeProvider(udfpsDisplayModeProvider)
178                     val animation = inflateUdfpsAnimation(this, controller)
179                     if (animation != null) {
180                         animation.init()
181                         animationViewController = animation
182                     }
183                     // This view overlaps the sensor area
184                     // prevent it from being selectable during a11y
185                     if (requestReason.isImportantForAccessibility()) {
186                         importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
187                     }
188 
189                     windowManager.addView(this, coreLayoutParams.updateDimensions(animation))
190                     sensorRect = sensorBounds
191                     touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
192                     overlayTouchListener = TouchExplorationStateChangeListener {
193                         if (accessibilityManager.isTouchExplorationEnabled) {
194                             setOnHoverListener { v, event -> onTouch(v, event, true) }
195                             setOnTouchListener(null)
196                             touchExplorationEnabled = true
197                         } else {
198                             setOnHoverListener(null)
199                             setOnTouchListener { v, event -> onTouch(v, event, true) }
200                             touchExplorationEnabled = false
201                         }
202                     }
203                     accessibilityManager.addTouchExplorationStateChangeListener(
204                         overlayTouchListener!!
205                     )
206                     overlayTouchListener?.onTouchExplorationStateChanged(true)
207                     useExpandedOverlay = featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)
208                 }
209             } catch (e: RuntimeException) {
210                 Log.e(TAG, "showUdfpsOverlay | failed to add window", e)
211             }
212             return true
213         }
214 
215         Log.v(TAG, "showUdfpsOverlay | the overlay is already showing")
216         return false
217     }
218 
219     fun inflateUdfpsAnimation(
220         view: UdfpsView,
221         controller: UdfpsController
222     ): UdfpsAnimationViewController<*>? {
223         val isEnrollment = when (requestReason) {
224             REASON_ENROLL_FIND_SENSOR, REASON_ENROLL_ENROLLING -> true
225             else -> false
226         }
227 
228         val filteredRequestReason = if (isEnrollment && shouldRemoveEnrollmentUi()) {
229             REASON_AUTH_OTHER
230         } else {
231             requestReason
232         }
233 
234         return when (filteredRequestReason) {
235             REASON_ENROLL_FIND_SENSOR,
236             REASON_ENROLL_ENROLLING -> {
237                 UdfpsEnrollViewController(
238                     view.addUdfpsView(R.layout.udfps_enroll_view) {
239                         updateSensorLocation(sensorBounds)
240                     },
241                     enrollHelper ?: throw IllegalStateException("no enrollment helper"),
242                     statusBarStateController,
243                     shadeExpansionStateManager,
244                     dialogManager,
245                     dumpManager,
246                     featureFlags,
247                     overlayParams.scaleFactor
248                 )
249             }
250             REASON_AUTH_KEYGUARD -> {
251                 UdfpsKeyguardViewController(
252                     view.addUdfpsView(R.layout.udfps_keyguard_view),
253                     statusBarStateController,
254                     shadeExpansionStateManager,
255                     statusBarKeyguardViewManager,
256                     keyguardUpdateMonitor,
257                     dumpManager,
258                     transitionController,
259                     configurationController,
260                     keyguardStateController,
261                     unlockedScreenOffAnimationController,
262                     dialogManager,
263                     controller,
264                     activityLaunchAnimator,
265                     featureFlags,
266                     primaryBouncerInteractor,
267                     alternateBouncerInteractor,
268                 )
269             }
270             REASON_AUTH_BP -> {
271                 // note: empty controller, currently shows no visual affordance
272                 UdfpsBpViewController(
273                     view.addUdfpsView(R.layout.udfps_bp_view),
274                     statusBarStateController,
275                     shadeExpansionStateManager,
276                     dialogManager,
277                     dumpManager
278                 )
279             }
280             REASON_AUTH_OTHER,
281             REASON_AUTH_SETTINGS -> {
282                 UdfpsFpmOtherViewController(
283                     view.addUdfpsView(R.layout.udfps_fpm_other_view),
284                     statusBarStateController,
285                     shadeExpansionStateManager,
286                     dialogManager,
287                     dumpManager
288                 )
289             }
290             else -> {
291                 Log.e(TAG, "Animation for reason $requestReason not supported yet")
292                 null
293             }
294         }
295     }
296 
297     /** Hide the overlay or return false and do nothing if it is already hidden. */
298     fun hide(): Boolean {
299         val wasShowing = isShowing
300 
301         overlayView?.apply {
302             if (isDisplayConfigured) {
303                 unconfigureDisplay()
304             }
305             windowManager.removeView(this)
306             setOnTouchListener(null)
307             setOnHoverListener(null)
308             animationViewController = null
309             overlayTouchListener?.let {
310                 accessibilityManager.removeTouchExplorationStateChangeListener(it)
311             }
312         }
313         overlayView = null
314         overlayTouchListener = null
315 
316         return wasShowing
317     }
318 
319     fun onEnrollmentProgress(remaining: Int) {
320         enrollHelper?.onEnrollmentProgress(remaining)
321     }
322 
323     fun onAcquiredGood() {
324         enrollHelper?.animateIfLastStep()
325     }
326 
327     fun onEnrollmentHelp() {
328         enrollHelper?.onEnrollmentHelp()
329     }
330 
331     /**
332      * This function computes the angle of touch relative to the sensor and maps
333      * the angle to a list of help messages which are announced if accessibility is enabled.
334      *
335      */
336     fun onTouchOutsideOfSensorArea(
337         touchX: Float,
338         touchY: Float,
339         sensorX: Float,
340         sensorY: Float,
341         rotation: Int
342     ) {
343 
344         if (!touchExplorationEnabled) {
345             return
346         }
347         val touchHints =
348             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
349         if (touchHints.size != 4) {
350             Log.e(TAG, "expected exactly 4 touch hints, got $touchHints.size?")
351             return
352         }
353         val theStr = onTouchOutsideOfSensorAreaImpl(touchX, touchY, sensorX, sensorY, rotation)
354         Log.v(TAG, "Announcing touch outside : " + theStr)
355         animationViewController?.doAnnounceForAccessibility(theStr)
356     }
357 
358     /**
359      * This function computes the angle of touch relative to the sensor and maps
360      * the angle to a list of help messages which are announced if accessibility is enabled.
361      *
362      * There are 4 quadrants of the circle (90 degree arcs)
363      *
364      * [315, 360] && [0, 45) -> touchHints[0] = "Move Fingerprint to the left"
365      * [45,  135)            -> touchHints[1] = "Move Fingerprint down"
366      * And so on.
367      */
368     fun onTouchOutsideOfSensorAreaImpl(
369         touchX: Float,
370         touchY: Float,
371         sensorX: Float,
372         sensorY: Float,
373         rotation: Int
374     ): String {
375         val touchHints =
376             context.resources.getStringArray(R.array.udfps_accessibility_touch_hints)
377 
378         val xRelativeToSensor = touchX - sensorX
379         // Touch coordinates are with respect to the upper left corner, so reverse
380         // this calculation
381         val yRelativeToSensor = sensorY - touchY
382 
383         var angleInRad =
384             Math.atan2(yRelativeToSensor.toDouble(), xRelativeToSensor.toDouble())
385         // If the radians are negative, that means we are counting clockwise.
386         // So we need to add 360 degrees
387         if (angleInRad < 0.0) {
388             angleInRad += 2.0 * Math.PI
389         }
390         // rad to deg conversion
391         val degrees = Math.toDegrees(angleInRad)
392 
393         val degreesPerBucket = 360.0 / touchHints.size
394         val halfBucketDegrees = degreesPerBucket / 2.0
395         // The mapping should be as follows
396         // [315, 360] && [0, 45] -> 0
397         // [45, 135]             -> 1
398         var index = (((degrees + halfBucketDegrees) % 360) / degreesPerBucket).toInt()
399         index %= touchHints.size
400 
401         // A rotation of 90 degrees corresponds to increasing the index by 1.
402         if (rotation == Surface.ROTATION_90) {
403             index = (index + 1) % touchHints.size
404         }
405 
406         if (rotation == Surface.ROTATION_270) {
407             index = (index + 3) % touchHints.size
408         }
409 
410         return touchHints[index]
411     }
412 
413     /** Cancel this request. */
414     fun cancel() {
415         try {
416             controllerCallback.onUserCanceled()
417         } catch (e: RemoteException) {
418             Log.e(TAG, "Remote exception", e)
419         }
420     }
421 
422     /** Checks if the id is relevant for this overlay. */
423     fun matchesRequestId(id: Long): Boolean = requestId == -1L || requestId == id
424 
425     private fun WindowManager.LayoutParams.updateDimensions(
426         animation: UdfpsAnimationViewController<*>?
427     ): WindowManager.LayoutParams {
428         val paddingX = animation?.paddingX ?: 0
429         val paddingY = animation?.paddingY ?: 0
430         if (animation != null && animation.listenForTouchesOutsideView()) {
431             flags = flags or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
432         }
433 
434         // Original sensorBounds assume portrait mode.
435         var rotatedBounds =
436             if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
437                 Rect(overlayParams.overlayBounds)
438             } else {
439                 Rect(overlayParams.sensorBounds)
440             }
441 
442         val rot = overlayParams.rotation
443         if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
444             if (!shouldRotate(animation)) {
445                 Log.v(
446                     TAG, "Skip rotating UDFPS bounds " + Surface.rotationToString(rot) +
447                             " animation=$animation" +
448                             " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" +
449                             " isOccluded=${keyguardStateController.isOccluded}"
450                 )
451             } else {
452                 Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot))
453                 RotationUtils.rotateBounds(
454                     rotatedBounds,
455                     overlayParams.naturalDisplayWidth,
456                     overlayParams.naturalDisplayHeight,
457                     rot
458                 )
459 
460                 if (featureFlags.isEnabled(Flags.UDFPS_NEW_TOUCH_DETECTION)) {
461                     RotationUtils.rotateBounds(
462                             sensorBounds,
463                             overlayParams.naturalDisplayWidth,
464                             overlayParams.naturalDisplayHeight,
465                             rot
466                     )
467                 }
468             }
469         }
470 
471         x = rotatedBounds.left - paddingX
472         y = rotatedBounds.top - paddingY
473         height = rotatedBounds.height() + 2 * paddingX
474         width = rotatedBounds.width() + 2 * paddingY
475 
476         return this
477     }
478 
479     private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean {
480         if (animation !is UdfpsKeyguardViewController) {
481             // always rotate view if we're not on the keyguard
482             return true
483         }
484 
485         // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded
486         return !(keyguardUpdateMonitor.isGoingToSleep || !keyguardStateController.isOccluded)
487     }
488 
489     private inline fun <reified T : View> UdfpsView.addUdfpsView(
490         @LayoutRes id: Int,
491         init: T.() -> Unit = {}
492     ): T {
493         val subView = inflater.inflate(id, null) as T
494         addView(subView)
495         subView.init()
496         return subView
497     }
498 }
499 
500 @ShowReason
isEnrollmentReasonnull501 private fun Int.isEnrollmentReason() =
502     this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING
503 
504 @ShowReason
505 private fun Int.isImportantForAccessibility() =
506     this == REASON_ENROLL_FIND_SENSOR ||
507             this == REASON_ENROLL_ENROLLING ||
508             this == REASON_AUTH_BP
509