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