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