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 package com.android.systemui.biometrics
17
18 import android.app.ActivityTaskManager
19 import android.content.Context
20 import android.content.res.Configuration
21 import android.graphics.Color
22 import android.graphics.PixelFormat
23 import android.graphics.PorterDuff
24 import android.graphics.PorterDuffColorFilter
25 import android.graphics.Rect
26 import android.hardware.biometrics.BiometricOverlayConstants
27 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD
28 import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS
29 import android.hardware.biometrics.SensorLocationInternal
30 import android.hardware.display.DisplayManager
31 import android.hardware.fingerprint.FingerprintManager
32 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
33 import android.hardware.fingerprint.ISidefpsController
34 import android.os.Handler
35 import android.util.Log
36 import android.util.RotationUtils
37 import android.view.Display
38 import android.view.DisplayInfo
39 import android.view.Gravity
40 import android.view.LayoutInflater
41 import android.view.Surface
42 import android.view.View
43 import android.view.View.AccessibilityDelegate
44 import android.view.ViewPropertyAnimator
45 import android.view.WindowManager
46 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
47 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
48 import android.view.accessibility.AccessibilityEvent
49 import androidx.annotation.RawRes
50 import com.airbnb.lottie.LottieAnimationView
51 import com.airbnb.lottie.LottieProperty
52 import com.airbnb.lottie.model.KeyPath
53 import com.android.app.animation.Interpolators
54 import com.android.internal.annotations.VisibleForTesting
55 import com.android.keyguard.KeyguardPINView
56 import com.android.systemui.Dumpable
57 import com.android.systemui.R
58 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
59 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
60 import com.android.systemui.dagger.SysUISingleton
61 import com.android.systemui.dagger.qualifiers.Application
62 import com.android.systemui.dagger.qualifiers.Main
63 import com.android.systemui.dump.DumpManager
64 import com.android.systemui.util.boundsOnScreen
65 import com.android.systemui.util.concurrency.DelayableExecutor
66 import com.android.systemui.util.traceSection
67 import java.io.PrintWriter
68 import javax.inject.Inject
69 import kotlinx.coroutines.CoroutineScope
70 import kotlinx.coroutines.launch
71
72 private const val TAG = "SideFpsController"
73
74 /**
75 * Shows and hides the side fingerprint sensor (side-fps) overlay and handles side fps touch events.
76 */
77 @SysUISingleton
78 class SideFpsController
79 @Inject
80 constructor(
81 private val context: Context,
82 private val layoutInflater: LayoutInflater,
83 fingerprintManager: FingerprintManager?,
84 private val windowManager: WindowManager,
85 private val activityTaskManager: ActivityTaskManager,
86 displayManager: DisplayManager,
87 private val displayStateInteractor: DisplayStateInteractor,
88 @Main private val mainExecutor: DelayableExecutor,
89 @Main private val handler: Handler,
90 private val alternateBouncerInteractor: AlternateBouncerInteractor,
91 @Application private val scope: CoroutineScope,
92 dumpManager: DumpManager
93 ) : Dumpable {
94 private val requests: HashSet<SideFpsUiRequestSource> = HashSet()
95
96 @VisibleForTesting
97 val sensorProps: FingerprintSensorPropertiesInternal =
98 fingerprintManager?.sideFpsSensorProperties
99 ?: throw IllegalStateException("no side fingerprint sensor")
100
101 @VisibleForTesting
102 val orientationReasonListener =
103 OrientationReasonListener(
104 context,
105 displayManager,
106 handler,
107 sensorProps,
108 { reason -> onOrientationChanged(reason) },
109 BiometricOverlayConstants.REASON_UNKNOWN
110 )
111
112 @VisibleForTesting val orientationListener = orientationReasonListener.orientationListener
113
114 private val isReverseDefaultRotation =
115 context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
116
117 private var overlayShowAnimator: ViewPropertyAnimator? = null
118
119 private var overlayView: View? = null
120 set(value) {
121 field?.let { oldView ->
122 val lottie = oldView.requireViewById(R.id.sidefps_animation) as LottieAnimationView
123 lottie.pauseAnimation()
124 windowManager.removeView(oldView)
125 orientationListener.disable()
126 }
127 overlayShowAnimator?.cancel()
128 overlayShowAnimator = null
129
130 field = value
131 field?.let { newView ->
132 if (requests.contains(SideFpsUiRequestSource.PRIMARY_BOUNCER)) {
133 newView.alpha = 0f
134 overlayShowAnimator =
135 newView
136 .animate()
137 .alpha(1f)
138 .setDuration(KeyguardPINView.ANIMATION_DURATION)
139 .setInterpolator(Interpolators.ALPHA_IN)
140 }
141 windowManager.addView(newView, overlayViewParams)
142 orientationListener.enable()
143 overlayShowAnimator?.start()
144 }
145 }
146 @VisibleForTesting var overlayOffsets: SensorLocationInternal = SensorLocationInternal.DEFAULT
147
148 private val displayInfo = DisplayInfo()
149
150 private val overlayViewParams =
151 WindowManager.LayoutParams(
152 WindowManager.LayoutParams.WRAP_CONTENT,
153 WindowManager.LayoutParams.WRAP_CONTENT,
154 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
155 Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
156 PixelFormat.TRANSLUCENT
157 )
158 .apply {
159 title = TAG
160 fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
161 gravity = Gravity.TOP or Gravity.LEFT
162 layoutInDisplayCutoutMode =
163 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
164 privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
165 }
166
167 init {
168 fingerprintManager?.setSidefpsController(
169 object : ISidefpsController.Stub() {
170 override fun show(
171 sensorId: Int,
172 @BiometricOverlayConstants.ShowReason reason: Int
173 ) =
174 if (reason.isReasonToAutoShow(activityTaskManager)) {
175 show(SideFpsUiRequestSource.AUTO_SHOW, reason)
176 } else {
177 hide(SideFpsUiRequestSource.AUTO_SHOW)
178 }
179
180 override fun hide(sensorId: Int) = hide(SideFpsUiRequestSource.AUTO_SHOW)
181 }
182 )
183 listenForAlternateBouncerVisibility()
184
185 dumpManager.registerDumpable(this)
186 }
187
188 private fun listenForAlternateBouncerVisibility() {
189 alternateBouncerInteractor.setAlternateBouncerUIAvailable(true, "SideFpsController")
190 scope.launch {
191 alternateBouncerInteractor.isVisible.collect { isVisible: Boolean ->
192 if (isVisible) {
193 show(SideFpsUiRequestSource.ALTERNATE_BOUNCER, REASON_AUTH_KEYGUARD)
194 } else {
195 hide(SideFpsUiRequestSource.ALTERNATE_BOUNCER)
196 }
197 }
198 }
199 }
200
201 /** Shows the side fps overlay if not already shown. */
202 fun show(
203 request: SideFpsUiRequestSource,
204 @BiometricOverlayConstants.ShowReason reason: Int = BiometricOverlayConstants.REASON_UNKNOWN
205 ) {
206 if (!displayStateInteractor.isInRearDisplayMode.value) {
207 requests.add(request)
208 mainExecutor.execute {
209 if (overlayView == null) {
210 traceSection(
211 "SideFpsController#show(request=${request.name}, reason=$reason)"
212 ) {
213 createOverlayForDisplay(reason)
214 }
215 } else {
216 Log.v(TAG, "overlay already shown")
217 }
218 }
219 }
220 }
221
222 /** Hides the fps overlay if shown. */
223 fun hide(request: SideFpsUiRequestSource) {
224 requests.remove(request)
225 mainExecutor.execute {
226 if (requests.isEmpty()) {
227 traceSection("SideFpsController#hide(${request.name})") { overlayView = null }
228 }
229 }
230 }
231
232 override fun dump(pw: PrintWriter, args: Array<out String>) {
233 pw.println("requests:")
234 for (requestSource in requests) {
235 pw.println(" $requestSource.name")
236 }
237
238 pw.println("overlayView:")
239 pw.println(" width=${overlayView?.width}")
240 pw.println(" height=${overlayView?.height}")
241 pw.println(" boundsOnScreen=${overlayView?.boundsOnScreen}")
242
243 pw.println("displayStateInteractor:")
244 pw.println(" isInRearDisplayMode=${displayStateInteractor?.isInRearDisplayMode?.value}")
245
246 pw.println("sensorProps:")
247 pw.println(" displayId=${displayInfo.uniqueId}")
248 pw.println(" sensorType=${sensorProps?.sensorType}")
249 pw.println(" location=${sensorProps?.getLocation(displayInfo.uniqueId)}")
250
251 pw.println("overlayOffsets=$overlayOffsets")
252 pw.println("isReverseDefaultRotation=$isReverseDefaultRotation")
253 pw.println("currentRotation=${displayInfo.rotation}")
254 }
255
256 private fun onOrientationChanged(@BiometricOverlayConstants.ShowReason reason: Int) {
257 if (overlayView != null) {
258 createOverlayForDisplay(reason)
259 }
260 }
261
262 private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) {
263 val view = layoutInflater.inflate(R.layout.sidefps_view, null, false)
264 overlayView = view
265 val display = context.display!!
266 // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation
267 display.getDisplayInfo(displayInfo)
268 val offsets =
269 sensorProps.getLocation(display.uniqueId).let { location ->
270 if (location == null) {
271 Log.w(TAG, "No location specified for display: ${display.uniqueId}")
272 }
273 location ?: sensorProps.location
274 }
275 overlayOffsets = offsets
276
277 val lottie = view.requireViewById(R.id.sidefps_animation) as LottieAnimationView
278 view.rotation =
279 display.asSideFpsAnimationRotation(
280 offsets.isYAligned(),
281 getRotationFromDefault(displayInfo.rotation)
282 )
283 lottie.setAnimation(
284 display.asSideFpsAnimation(
285 offsets.isYAligned(),
286 getRotationFromDefault(displayInfo.rotation)
287 )
288 )
289 lottie.addLottieOnCompositionLoadedListener {
290 // Check that view is not stale, and that overlayView has not been hidden/removed
291 if (overlayView != null && overlayView == view) {
292 updateOverlayParams(display, it.bounds)
293 }
294 }
295 orientationReasonListener.reason = reason
296 lottie.addOverlayDynamicColor(context, reason)
297
298 /**
299 * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from
300 * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is
301 * in focus
302 */
303 view.setAccessibilityDelegate(
304 object : AccessibilityDelegate() {
305 override fun dispatchPopulateAccessibilityEvent(
306 host: View,
307 event: AccessibilityEvent
308 ): Boolean {
309 return if (
310 event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
311 ) {
312 true
313 } else {
314 super.dispatchPopulateAccessibilityEvent(host, event)
315 }
316 }
317 }
318 )
319 }
320
321 @VisibleForTesting
322 fun updateOverlayParams(display: Display, bounds: Rect) {
323 val isNaturalOrientation = display.isNaturalOrientation()
324 val isDefaultOrientation =
325 if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation
326 val size = windowManager.maximumWindowMetrics.bounds
327
328 val displayWidth = if (isDefaultOrientation) size.width() else size.height()
329 val displayHeight = if (isDefaultOrientation) size.height() else size.width()
330 val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height()
331 val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width()
332
333 val sensorBounds =
334 if (overlayOffsets.isYAligned()) {
335 Rect(
336 displayWidth - boundsWidth,
337 overlayOffsets.sensorLocationY,
338 displayWidth,
339 overlayOffsets.sensorLocationY + boundsHeight
340 )
341 } else {
342 Rect(
343 overlayOffsets.sensorLocationX,
344 0,
345 overlayOffsets.sensorLocationX + boundsWidth,
346 boundsHeight
347 )
348 }
349
350 RotationUtils.rotateBounds(
351 sensorBounds,
352 Rect(0, 0, displayWidth, displayHeight),
353 getRotationFromDefault(display.rotation)
354 )
355
356 overlayViewParams.x = sensorBounds.left
357 overlayViewParams.y = sensorBounds.top
358
359 windowManager.updateViewLayout(overlayView, overlayViewParams)
360 }
361
362 private fun getRotationFromDefault(rotation: Int): Int =
363 if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation
364 }
365
366 private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal?
<lambda>null367 get() = this?.sensorPropertiesInternal?.firstOrNull { it.isAnySidefpsType }
368
369 /** Returns [True] when the device has a side fingerprint sensor. */
FingerprintManagernull370 fun FingerprintManager?.hasSideFpsSensor(): Boolean = this?.sideFpsSensorProperties != null
371
372 @BiometricOverlayConstants.ShowReason
373 private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Boolean =
374 when (this) {
375 REASON_AUTH_KEYGUARD -> false
376 REASON_AUTH_SETTINGS ->
377 when (activityTaskManager.topClass()) {
378 // TODO(b/186176653): exclude fingerprint overlays from this list view
379 "com.android.settings.biometrics.fingerprint.FingerprintSettings" -> false
380 else -> true
381 }
382 else -> true
383 }
384
topClassnull385 private fun ActivityTaskManager.topClass(): String =
386 getTasks(1).firstOrNull()?.topActivity?.className ?: ""
387
388 @RawRes
389 private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int =
390 when (rotationFromDefault) {
391 Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
392 Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape
393 else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse
394 }
395
asSideFpsAnimationRotationnull396 private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float =
397 when (rotationFromDefault) {
398 Surface.ROTATION_90 -> if (yAligned) 0f else 180f
399 Surface.ROTATION_180 -> 180f
400 Surface.ROTATION_270 -> if (yAligned) 180f else 0f
401 else -> 0f
402 }
403
SensorLocationInternalnull404 private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0
405
406 private fun Display.isNaturalOrientation(): Boolean =
407 rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
408
409 private fun LottieAnimationView.addOverlayDynamicColor(
410 context: Context,
411 @BiometricOverlayConstants.ShowReason reason: Int
412 ) {
413 fun update() {
414 val isKeyguard = reason == REASON_AUTH_KEYGUARD
415 if (isKeyguard) {
416 val color =
417 com.android.settingslib.Utils.getColorAttrDefaultColor(
418 context,
419 com.android.internal.R.attr.materialColorPrimaryFixed
420 )
421 val outerRimColor =
422 com.android.settingslib.Utils.getColorAttrDefaultColor(
423 context,
424 com.android.internal.R.attr.materialColorPrimaryFixedDim
425 )
426 val chevronFill =
427 com.android.settingslib.Utils.getColorAttrDefaultColor(
428 context,
429 com.android.internal.R.attr.materialColorOnPrimaryFixed
430 )
431 addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) {
432 PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
433 }
434 addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) {
435 PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP)
436 }
437 addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
438 PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP)
439 }
440 } else {
441 if (!isDarkMode(context)) {
442 addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) {
443 PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
444 }
445 }
446 for (key in listOf(".blue600", ".blue400")) {
447 addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) {
448 PorterDuffColorFilter(
449 context.getColor(R.color.settingslib_color_blue400),
450 PorterDuff.Mode.SRC_ATOP
451 )
452 }
453 }
454 }
455 }
456
457 if (composition != null) {
458 update()
459 } else {
460 addLottieOnCompositionLoadedListener { update() }
461 }
462 }
463
isDarkModenull464 private fun isDarkMode(context: Context): Boolean {
465 val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
466 return darkMode == Configuration.UI_MODE_NIGHT_YES
467 }
468
469 @VisibleForTesting
470 class OrientationReasonListener(
471 context: Context,
472 displayManager: DisplayManager,
473 handler: Handler,
474 sensorProps: FingerprintSensorPropertiesInternal,
475 onOrientationChanged: (reason: Int) -> Unit,
476 @BiometricOverlayConstants.ShowReason var reason: Int
477 ) {
478 val orientationListener =
479 BiometricDisplayListener(
480 context,
481 displayManager,
482 handler,
483 BiometricDisplayListener.SensorType.SideFingerprint(sensorProps)
<lambda>null484 ) {
485 onOrientationChanged(reason)
486 }
487 }
488
489 /**
490 * The source of a request to show the side fps visual indicator. This is distinct from
491 * [BiometricOverlayConstants] which corrresponds with the reason fingerprint authentication is
492 * requested.
493 */
494 enum class SideFpsUiRequestSource {
495 /** see [isReasonToAutoShow] */
496 AUTO_SHOW,
497 /** Pin, pattern or password bouncer */
498 PRIMARY_BOUNCER,
499 ALTERNATE_BOUNCER
500 }
501