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