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