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