| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.systemui.biometrics |
| |
| import android.animation.ValueAnimator |
| import android.graphics.PointF |
| import android.graphics.RectF |
| import androidx.annotation.VisibleForTesting |
| import androidx.lifecycle.Lifecycle |
| import androidx.lifecycle.repeatOnLifecycle |
| import com.android.app.animation.Interpolators |
| import com.android.systemui.Dumpable |
| import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor |
| import com.android.systemui.dump.DumpManager |
| import com.android.systemui.lifecycle.repeatWhenAttached |
| import com.android.systemui.plugins.statusbar.StatusBarStateController |
| import com.android.systemui.statusbar.StatusBarState.SHADE |
| import com.android.systemui.statusbar.phone.SystemUIDialogManager |
| import com.android.systemui.util.ViewController |
| import kotlinx.coroutines.CoroutineScope |
| import kotlinx.coroutines.Job |
| import kotlinx.coroutines.flow.map |
| import kotlinx.coroutines.launch |
| import java.io.PrintWriter |
| |
| /** |
| * Handles: |
| * 1. registering for listeners when its view is attached and unregistering on view detached |
| * 2. pausing UDFPS when FingerprintManager may still be running but we temporarily want to hide |
| * the affordance. this allows us to fade the view in and out nicely (see shouldPauseAuth) |
| * 3. sending events to its view including: |
| * - enabling and disabling of the UDFPS display mode |
| * - sensor position changes |
| * - doze time event |
| */ |
| abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( |
| view: T, |
| protected val statusBarStateController: StatusBarStateController, |
| protected val primaryBouncerInteractor: PrimaryBouncerInteractor, |
| protected val dialogManager: SystemUIDialogManager, |
| private val dumpManager: DumpManager |
| ) : ViewController<T>(view), Dumpable { |
| |
| protected abstract val tag: String |
| |
| private val view: T |
| get() = mView!! |
| |
| private var dialogAlphaAnimator: ValueAnimator? = null |
| private val dialogListener = SystemUIDialogManager.Listener { runDialogAlphaAnimator() } |
| |
| /** If the notification shade is visible. */ |
| var notificationShadeVisible: Boolean = false |
| |
| /** |
| * The amount of translation needed if the view currently requires the user to touch |
| * somewhere other than the exact center of the sensor. For example, this can happen |
| * during guided enrollment. |
| */ |
| open val touchTranslation: PointF = PointF(0f, 0f) |
| |
| /** |
| * X-Padding to add to left and right of the sensor rectangle area to increase the size of our |
| * window to draw within. |
| */ |
| open val paddingX: Int = 0 |
| |
| /** |
| * Y-Padding to add to top and bottom of the sensor rectangle area to increase the size of our |
| * window to draw within. |
| */ |
| open val paddingY: Int = 0 |
| |
| open fun updateAlpha() { |
| view.updateAlpha() |
| } |
| |
| init { |
| view.repeatWhenAttached { |
| // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion |
| // can make the view not visible; and we still want to listen for events |
| // that may make the view visible again. |
| repeatOnLifecycle(Lifecycle.State.CREATED) { |
| listenForBouncerExpansion(this) |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| open suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job { |
| return scope.launch { |
| primaryBouncerInteractor.bouncerExpansion.map { 1f - it }.collect { expansion: Float -> |
| if (statusBarStateController.state != SHADE) { |
| notificationShadeVisible = expansion > 0f |
| view.onExpansionChanged(expansion) |
| updatePauseAuth() |
| } |
| } |
| } |
| } |
| |
| fun runDialogAlphaAnimator() { |
| val hideAffordance = dialogManager.shouldHideAffordance() |
| dialogAlphaAnimator?.cancel() |
| dialogAlphaAnimator = ValueAnimator.ofFloat( |
| view.calculateAlpha() / 255f, |
| if (hideAffordance) 0f else 1f) |
| .apply { |
| duration = if (hideAffordance) 83L else 200L |
| interpolator = if (hideAffordance) Interpolators.LINEAR else Interpolators.ALPHA_IN |
| |
| addUpdateListener { animatedValue -> |
| view.setDialogSuggestedAlpha(animatedValue.animatedValue as Float) |
| updateAlpha() |
| updatePauseAuth() |
| } |
| start() |
| } |
| } |
| |
| override fun onViewAttached() { |
| dialogManager.registerListener(dialogListener) |
| dumpManager.registerDumpable(dumpTag, this) |
| } |
| |
| override fun onViewDetached() { |
| dialogManager.unregisterListener(dialogListener) |
| dumpManager.unregisterDumpable(dumpTag) |
| } |
| |
| /** |
| * in some cases, onViewAttached is called for the newly added view using an instance of |
| * this controller before onViewDetached is called on the previous view, so we must have a |
| * unique [dumpTag] per instance of this class. |
| */ |
| private val dumpTag = "$tag ($this)" |
| |
| override fun dump(pw: PrintWriter, args: Array<String>) { |
| pw.println("mNotificationShadeVisible=$notificationShadeVisible") |
| pw.println("shouldPauseAuth()=" + shouldPauseAuth()) |
| pw.println("isPauseAuth=" + view.isPauseAuth) |
| pw.println("dialogSuggestedAlpha=" + view.dialogSuggestedAlpha) |
| } |
| |
| /** |
| * Returns true if the fingerprint manager is running, but we want to temporarily pause |
| * authentication. |
| */ |
| open fun shouldPauseAuth(): Boolean { |
| return notificationShadeVisible || dialogManager.shouldHideAffordance() |
| } |
| |
| /** |
| * Send pause auth update to our view. |
| */ |
| fun updatePauseAuth() { |
| if (view.setPauseAuth(shouldPauseAuth())) { |
| view.postInvalidate() |
| } |
| } |
| |
| /** |
| * Send sensor position change to our view. This rect contains paddingX and paddingY. |
| */ |
| fun onSensorRectUpdated(sensorRect: RectF) { |
| view.onSensorRectUpdated(sensorRect) |
| } |
| |
| /** |
| * Send dozeTimeTick to view in case it wants to handle its burn-in offset. |
| */ |
| fun dozeTimeTick() { |
| if (view.dozeTimeTick()) { |
| view.postInvalidate() |
| } |
| } |
| |
| /** |
| * The display began transitioning into the UDFPS mode and the fingerprint manager started |
| * authenticating. |
| */ |
| fun onDisplayConfiguring() { |
| view.onDisplayConfiguring() |
| view.postInvalidate() |
| } |
| |
| /** |
| * The display transitioned away from the UDFPS mode and the fingerprint manager stopped |
| * authenticating. |
| */ |
| fun onDisplayUnconfigured() { |
| view.onDisplayUnconfigured() |
| view.postInvalidate() |
| } |
| |
| /** |
| * Whether to listen for touches outside of the view. |
| */ |
| open fun listenForTouchesOutsideView(): Boolean = false |
| |
| /** |
| * Called when a view should announce an accessibility event. |
| */ |
| open fun doAnnounceForAccessibility(str: String) {} |
| } |