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