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.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
isImportantForAccessibilitynull460 private fun Int.isImportantForAccessibility() =
461 this == REASON_ENROLL_FIND_SENSOR ||
462 this == REASON_ENROLL_ENROLLING ||
463 this == REASON_AUTH_BP
464