• 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 
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