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.BiometricRequestConstants.REASON_AUTH_BP
25 import android.hardware.biometrics.BiometricRequestConstants.REASON_AUTH_KEYGUARD
26 import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_ENROLLING
27 import android.hardware.biometrics.BiometricRequestConstants.REASON_ENROLL_FIND_SENSOR
28 import android.hardware.biometrics.BiometricRequestConstants.RequestReason
29 import android.hardware.fingerprint.IUdfpsOverlayControllerCallback
30 import android.os.Build
31 import android.os.RemoteException
32 import android.os.Trace
33 import android.provider.Settings
34 import android.util.Log
35 import android.util.RotationUtils
36 import android.view.LayoutInflater
37 import android.view.MotionEvent
38 import android.view.Surface
39 import android.view.View
40 import android.view.WindowManager
41 import android.view.accessibility.AccessibilityManager
42 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
43 import androidx.annotation.VisibleForTesting
44 import com.android.app.tracing.coroutines.launchTraced as launch
45 import com.android.app.viewcapture.ViewCaptureAwareWindowManager
46 import com.android.keyguard.KeyguardUpdateMonitor
47 import com.android.systemui.animation.ActivityTransitionAnimator
48 import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
49 import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
50 import com.android.systemui.biometrics.ui.binder.UdfpsTouchOverlayBinder
51 import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay
52 import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayViewModel
53 import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel
54 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
55 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
56 import com.android.systemui.dagger.qualifiers.Application
57 import com.android.systemui.dump.DumpManager
58 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
59 import com.android.systemui.keyguard.shared.model.KeyguardState
60 import com.android.systemui.plugins.statusbar.StatusBarStateController
61 import com.android.systemui.power.domain.interactor.PowerInteractor
62 import com.android.systemui.res.R
63 import com.android.systemui.shade.domain.interactor.ShadeInteractor
64 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
65 import com.android.systemui.statusbar.phone.SystemUIDialogManager
66 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
67 import com.android.systemui.statusbar.policy.ConfigurationController
68 import com.android.systemui.statusbar.policy.KeyguardStateController
69 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
70 import dagger.Lazy
71 import kotlinx.coroutines.CoroutineScope
72 import kotlinx.coroutines.ExperimentalCoroutinesApi
73 import kotlinx.coroutines.Job
74 import kotlinx.coroutines.flow.Flow
75 import kotlinx.coroutines.flow.filter
76 import kotlinx.coroutines.flow.map
77 
78 private const val TAG = "UdfpsControllerOverlay"
79 
80 @VisibleForTesting const val SETTING_REMOVE_ENROLLMENT_UI = "udfps_overlay_remove_enrollment_ui"
81 
82 /**
83  * Keeps track of the overlay state and UI resources associated with a single FingerprintService
84  * request. This state can persist across configuration changes via the [show] and [hide] methods.
85  */
86 @ExperimentalCoroutinesApi
87 @UiThread
88 class UdfpsControllerOverlay
89 @JvmOverloads
90 constructor(
91     private val context: Context,
92     private val inflater: LayoutInflater,
93     private val windowManager: ViewCaptureAwareWindowManager,
94     private val accessibilityManager: AccessibilityManager,
95     private val statusBarStateController: StatusBarStateController,
96     private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
97     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
98     private val dialogManager: SystemUIDialogManager,
99     private val dumpManager: DumpManager,
100     private val configurationController: ConfigurationController,
101     private val keyguardStateController: KeyguardStateController,
102     private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
103     private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
104     val requestId: Long,
105     @RequestReason val requestReason: Int,
106     private val controllerCallback: IUdfpsOverlayControllerCallback,
107     private val onTouch: (View, MotionEvent) -> Boolean,
108     private val activityTransitionAnimator: ActivityTransitionAnimator,
109     private val primaryBouncerInteractor: PrimaryBouncerInteractor,
110     private val alternateBouncerInteractor: AlternateBouncerInteractor,
111     private val isDebuggable: Boolean = Build.IS_DEBUGGABLE,
112     private val udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate,
113     private val transitionInteractor: KeyguardTransitionInteractor,
114     private val selectedUserInteractor: SelectedUserInteractor,
115     private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>,
116     private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>,
117     private val shadeInteractor: ShadeInteractor,
118     private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
119     private val powerInteractor: PowerInteractor,
120     @Application private val scope: CoroutineScope,
121 ) {
122     private val currentStateUpdatedToOffAodOrDozing: Flow<Unit> =
123         transitionInteractor.currentKeyguardState
124             .filter {
125                 it == KeyguardState.OFF || it == KeyguardState.AOD || it == KeyguardState.DOZING
126             }
127             .map {} // map to Unit
128     private var listenForCurrentKeyguardState: Job? = null
129     private var addViewRunnable: Runnable? = null
130     private var overlayTouchView: UdfpsTouchOverlay? = null
131 
132     /**
133      * Get the current UDFPS overlay touch view
134      *
135      * @return The view, when [isShowing], else null
136      */
137     fun getTouchOverlay(): View? {
138         return overlayTouchView
139     }
140 
141     private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams()
142     private var sensorBounds: Rect = Rect()
143 
144     private var overlayTouchListener: TouchExplorationStateChangeListener? = null
145 
146     private val coreLayoutParams =
147         WindowManager.LayoutParams(
148                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
149                 0 /* flags set in computeLayoutParams() */,
150                 PixelFormat.TRANSLUCENT,
151             )
152             .apply {
153                 title = TAG
154                 fitInsetsTypes = 0
155                 gravity = android.view.Gravity.TOP or android.view.Gravity.LEFT
156                 layoutInDisplayCutoutMode =
157                     WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
158                 flags =
159                     (Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS or
160                         WindowManager.LayoutParams.FLAG_SPLIT_TOUCH)
161                 privateFlags =
162                     WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY or
163                         WindowManager.LayoutParams.PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION
164                 // Avoid announcing window title.
165                 accessibilityTitle = " "
166                 inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY
167             }
168 
169     /** If the overlay is currently showing. */
170     val isShowing: Boolean
171         get() = getTouchOverlay() != null
172 
173     /** Opposite of [isShowing]. */
174     val isHiding: Boolean
175         get() = getTouchOverlay() == null
176 
177     private var touchExplorationEnabled = false
178 
179     private fun shouldRemoveEnrollmentUi(): Boolean {
180         if (isDebuggable) {
181             return Settings.Global.getInt(
182                 context.contentResolver,
183                 SETTING_REMOVE_ENROLLMENT_UI,
184                 0, /* def */
185             ) != 0
186         }
187         return false
188     }
189 
190     /** Show the overlay or return false and do nothing if it is already showing. */
191     @SuppressLint("ClickableViewAccessibility")
192     fun show(controller: UdfpsController, params: UdfpsOverlayParams): Boolean {
193         if (getTouchOverlay() == null) {
194             overlayParams = params
195             sensorBounds = Rect(params.sensorBounds)
196             try {
197                 overlayTouchView =
198                     (inflater.inflate(R.layout.udfps_touch_overlay, null, false)
199                             as UdfpsTouchOverlay)
200                         .apply {
201                             // This view overlaps the sensor area
202                             // prevent it from being selectable during a11y
203                             if (requestReason.isImportantForAccessibility()) {
204                                 importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
205                             }
206 
207                             addViewNowOrLater(this, null)
208                             when (requestReason) {
209                                 REASON_AUTH_KEYGUARD ->
210                                     UdfpsTouchOverlayBinder.bind(
211                                         view = this,
212                                         viewModel = deviceEntryUdfpsTouchOverlayViewModel.get(),
213                                         udfpsOverlayInteractor = udfpsOverlayInteractor,
214                                     )
215                                 else ->
216                                     UdfpsTouchOverlayBinder.bind(
217                                         view = this,
218                                         viewModel = defaultUdfpsTouchOverlayViewModel.get(),
219                                         udfpsOverlayInteractor = udfpsOverlayInteractor,
220                                     )
221                             }
222                         }
223 
224                 getTouchOverlay()?.apply {
225                     touchExplorationEnabled = accessibilityManager.isTouchExplorationEnabled
226                     overlayTouchListener = TouchExplorationStateChangeListener {
227                         if (accessibilityManager.isTouchExplorationEnabled) {
228                             setOnHoverListener { v, event -> onTouch(v, event) }
229                             setOnTouchListener(null)
230                             touchExplorationEnabled = true
231                         } else {
232                             setOnHoverListener(null)
233                             setOnTouchListener { v, event -> onTouch(v, event) }
234                             touchExplorationEnabled = false
235                         }
236                     }
237                     accessibilityManager.addTouchExplorationStateChangeListener(
238                         overlayTouchListener!!
239                     )
240                     overlayTouchListener?.onTouchExplorationStateChanged(true)
241                 }
242             } catch (e: RuntimeException) {
243                 Log.e(TAG, "showUdfpsOverlay | failed to add window", e)
244             }
245             return true
246         }
247 
248         Log.d(TAG, "showUdfpsOverlay | the overlay is already showing")
249         return false
250     }
251 
252     private fun addViewNowOrLater(view: View, animation: UdfpsAnimationViewController<*>?) {
253         addViewRunnable =
254             kotlinx.coroutines.Runnable {
255                 Trace.setCounter("UdfpsAddView", 1)
256                 windowManager.addView(view, coreLayoutParams.updateDimensions(animation))
257             }
258         if (powerInteractor.detailedWakefulness.value.isAwake()) {
259             // Device is awake, so we add the view immediately.
260             addViewIfPending()
261         } else {
262             listenForCurrentKeyguardState?.cancel()
263             listenForCurrentKeyguardState =
264                 scope.launch { currentStateUpdatedToOffAodOrDozing.collect { addViewIfPending() } }
265         }
266     }
267 
268     private fun addViewIfPending() {
269         addViewRunnable?.let {
270             listenForCurrentKeyguardState?.cancel()
271             it.run()
272         }
273         addViewRunnable = null
274     }
275 
276     fun updateOverlayParams(updatedOverlayParams: UdfpsOverlayParams) {
277         overlayParams = updatedOverlayParams
278         sensorBounds = updatedOverlayParams.sensorBounds
279         getTouchOverlay()?.let {
280             if (addViewRunnable == null) {
281                 // Only updateViewLayout if there's no pending view to add to WM.
282                 // If there is a pending view, that means the view hasn't been added yet so there's
283                 // no need to update any layouts. Instead the correct params will be used when the
284                 // view is eventually added.
285                 windowManager.updateViewLayout(it, coreLayoutParams.updateDimensions(null))
286             }
287         }
288     }
289 
290     /** Hide the overlay or return false and do nothing if it is already hidden. */
291     fun hide(): Boolean {
292         val wasShowing = isShowing
293 
294         udfpsDisplayModeProvider.disable(null)
295         getTouchOverlay()?.apply {
296             if (this.parent != null) {
297                 windowManager.removeView(this)
298             }
299             Trace.setCounter("UdfpsAddView", 0)
300             setOnTouchListener(null)
301             setOnHoverListener(null)
302             overlayTouchListener?.let {
303                 accessibilityManager.removeTouchExplorationStateChangeListener(it)
304             }
305         }
306 
307         overlayTouchView = null
308         overlayTouchListener = null
309         listenForCurrentKeyguardState?.cancel()
310 
311         return wasShowing
312     }
313 
314     /** Cancel this request. */
315     fun cancel() {
316         try {
317             controllerCallback.onUserCanceled()
318         } catch (e: RemoteException) {
319             Log.e(TAG, "Remote exception", e)
320         }
321     }
322 
323     /** Checks if the id is relevant for this overlay. */
324     fun matchesRequestId(id: Long): Boolean = requestId == -1L || requestId == id
325 
326     private fun WindowManager.LayoutParams.updateDimensions(
327         animation: UdfpsAnimationViewController<*>?
328     ): WindowManager.LayoutParams {
329         val paddingX = animation?.paddingX ?: 0
330         val paddingY = animation?.paddingY ?: 0
331 
332         val isEnrollment =
333             when (requestReason) {
334                 REASON_ENROLL_FIND_SENSOR,
335                 REASON_ENROLL_ENROLLING -> true
336                 else -> false
337             }
338 
339         // Use expanded overlay unless touchExploration enabled
340         var rotatedBounds =
341             if (accessibilityManager.isTouchExplorationEnabled && isEnrollment) {
342                 Rect(overlayParams.sensorBounds)
343             } else {
344                 Rect(0, 0, overlayParams.naturalDisplayWidth, overlayParams.naturalDisplayHeight)
345             }
346 
347         val rot = overlayParams.rotation
348         if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) {
349             if (!shouldRotate(animation)) {
350                 Log.v(
351                     TAG,
352                     "Skip rotating UDFPS bounds " +
353                         Surface.rotationToString(rot) +
354                         " animation=$animation" +
355                         " isGoingToSleep=${keyguardUpdateMonitor.isGoingToSleep}" +
356                         " isOccluded=${keyguardStateController.isOccluded}",
357                 )
358             } else {
359                 Log.v(TAG, "Rotate UDFPS bounds " + Surface.rotationToString(rot))
360                 RotationUtils.rotateBounds(
361                     rotatedBounds,
362                     overlayParams.naturalDisplayWidth,
363                     overlayParams.naturalDisplayHeight,
364                     rot,
365                 )
366 
367                 RotationUtils.rotateBounds(
368                     sensorBounds,
369                     overlayParams.naturalDisplayWidth,
370                     overlayParams.naturalDisplayHeight,
371                     rot,
372                 )
373             }
374         }
375 
376         x = rotatedBounds.left - paddingX
377         y = rotatedBounds.top - paddingY
378         height = rotatedBounds.height() + 2 * paddingX
379         width = rotatedBounds.width() + 2 * paddingY
380 
381         return this
382     }
383 
384     private fun shouldRotate(animation: UdfpsAnimationViewController<*>?): Boolean {
385         if (!keyguardStateController.isShowing) {
386             // always rotate view if we're not on the keyguard
387             return true
388         }
389 
390         // on the keyguard, make sure we don't rotate if we're going to sleep or not occluded
391         return !(keyguardUpdateMonitor.isGoingToSleep || !keyguardStateController.isOccluded)
392     }
393 }
394 
395 @RequestReason
isImportantForAccessibilitynull396 private fun Int.isImportantForAccessibility() =
397     this == REASON_ENROLL_FIND_SENSOR || this == REASON_ENROLL_ENROLLING || this == REASON_AUTH_BP
398