Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 764d2816 authored by Beverly's avatar Beverly
Browse files

Support face auth ripple animation

Initialize AuthRipple as part of the notification shade in the
StatusBarModule instead of adding the view later.

First take face auth ripple.

Test: atest AuthRippleControllerTest
Bug: 182316496
Bug: 175717712
Change-Id: Iafeb5b1ff95a8e27e849c775b42fb6e9940b842d
parent 0980b0a0
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -100,4 +100,12 @@
            android:ellipsize="marquee"
            android:focusable="true" />
    </LinearLayout>

    <com.android.systemui.biometrics.AuthRippleView
        android:id="@+id/auth_ripple"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:importantForAccessibility="no"
        sysui:ignoreRightInset="true"
    />
</com.android.systemui.statusbar.phone.NotificationShadeWindowView>
+7 −0
Original line number Diff line number Diff line
@@ -642,4 +642,11 @@

    <!-- Whether to use window background blur for the volume dialog. -->
    <bool name="config_volumeDialogUseBackgroundBlur">false</bool>

    <!-- The properties of the face auth camera in pixels -->
    <integer-array name="config_face_auth_props">
        <!-- sensorLocationX -->
        <!-- sensorLocationY -->
        <!--sensorRadius -->
    </integer-array>
</resources>
+37 −2
Original line number Diff line number Diff line
@@ -29,6 +29,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.graphics.PointF;
import android.graphics.RectF;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricPrompt;
@@ -82,6 +83,7 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
    @Nullable private final List<FingerprintSensorPropertiesInternal> mFpProps;
    @Nullable private final List<FaceSensorPropertiesInternal> mFaceProps;
    @Nullable private final List<FingerprintSensorPropertiesInternal> mUdfpsProps;
    @Nullable private final PointF mFaceAuthSensorLocation;

    // TODO: These should just be saved from onSaveState
    private SomeArgs mCurrentDialogArgs;
@@ -261,10 +263,34 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
    }

    /**
     * @return where the UDFPS exists on the screen in pixels.
     * @return where the UDFPS exists on the screen in pixels in portrait mode.
     */
    public RectF getUdfpsRegion() {
        return mUdfpsController == null ? null : mUdfpsController.getSensorLocation();
        return mUdfpsController == null
                ? null
                : mUdfpsController.getSensorLocation();
    }

    /**
     * @return where the UDFPS exists on the screen in pixels in portrait mode.
     */
    public PointF getUdfpsSensorLocation() {
        if (mUdfpsController == null) {
            return null;
        }
        return new PointF(mUdfpsController.getSensorLocation().centerX(),
                mUdfpsController.getSensorLocation().centerY());
    }

    /**
     * @return where the face authentication sensor exists relative to the screen in pixels in
     * portrait mode.
     */
    public PointF getFaceAuthSensorLocation() {
        if (mFaceProps == null || mFaceAuthSensorLocation == null) {
            return null;
        }
        return new PointF(mFaceAuthSensorLocation.x, mFaceAuthSensorLocation.y);
    }

    /**
@@ -339,6 +365,15 @@ public class AuthController extends SystemUI implements CommandQueue.Callbacks,
            }
        }
        mUdfpsProps = !udfpsProps.isEmpty() ? udfpsProps : null;
        int[] faceAuthLocation = context.getResources().getIntArray(
                com.android.systemui.R.array.config_face_auth_props);
        if (faceAuthLocation == null || faceAuthLocation.length < 2) {
            mFaceAuthSensorLocation = null;
        } else {
            mFaceAuthSensorLocation = new PointF(
                    (float) faceAuthLocation[0],
                    (float) faceAuthLocation[1]);
        }

        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+113 −52
Original line number Diff line number Diff line
@@ -17,17 +17,19 @@
package com.android.systemui.biometrics

import android.content.Context
import android.content.res.Configuration
import android.graphics.PointF
import android.hardware.biometrics.BiometricSourceType
import android.view.View
import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.Utils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.phone.dagger.StatusBarComponent.StatusBarScope
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.ViewController
import java.io.PrintWriter
import javax.inject.Inject

@@ -35,30 +37,82 @@ import javax.inject.Inject
 * Controls the ripple effect that shows when authentication is successful.
 * The ripple uses the accent color of the current theme.
 */
@SysUISingleton
@StatusBarScope
class AuthRippleController @Inject constructor(
    commandRegistry: CommandRegistry,
    configurationController: ConfigurationController,
    private val context: Context,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor
) {
    private val sysuiContext: Context,
    private val authController: AuthController,
    private val configurationController: ConfigurationController,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
    private val commandRegistry: CommandRegistry,
    private val notificationShadeWindowController: NotificationShadeWindowController,
    rippleView: AuthRippleView?
) : ViewController<AuthRippleView>(rippleView) {
    private var fingerprintSensorLocation: PointF? = null
    private var faceSensorLocation: PointF? = null

    @VisibleForTesting
    var rippleView: AuthRippleView = AuthRippleView(context, attrs = null)
    public override fun onViewAttached() {
        updateRippleColor()
        updateSensorLocation()
        configurationController.addCallback(configurationChangedListener)
        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
        commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
    }

    @VisibleForTesting
    public override fun onViewDetached() {
        keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
        configurationController.removeCallback(configurationChangedListener)
        commandRegistry.unregisterCommand("auth-ripple")

        notificationShadeWindowController.setForcePluginOpen(false, this)
    }

    private fun showRipple(biometricSourceType: BiometricSourceType?) {
        if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
            fingerprintSensorLocation != null) {
            mView.setSensorLocation(fingerprintSensorLocation!!)
            showRipple()
        } else if (biometricSourceType == BiometricSourceType.FACE &&
            faceSensorLocation != null) {
            mView.setSensorLocation(faceSensorLocation!!)
            showRipple()
        }
    }

    private fun showRipple() {
        notificationShadeWindowController.setForcePluginOpen(true, this)
        mView.startRipple(Runnable {
            notificationShadeWindowController.setForcePluginOpen(false, this)
        })
    }

    private fun updateSensorLocation() {
        fingerprintSensorLocation = authController.udfpsSensorLocation
        faceSensorLocation = authController.faceAuthSensorLocation
    }

    private fun updateRippleColor() {
        mView.setColor(
            Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor)
    }

    val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
    val keyguardUpdateMonitorCallback =
        object : KeyguardUpdateMonitorCallback() {
            override fun onBiometricAuthenticated(
                userId: Int,
                biometricSourceType: BiometricSourceType?,
                isStrongBiometric: Boolean
            ) {
            if (biometricSourceType == BiometricSourceType.FINGERPRINT) {
                rippleView.startRipple()
            }
                showRipple(biometricSourceType)
            }
    }

    init {
        val configurationChangedListener = object : ConfigurationController.ConfigurationListener {
    val configurationChangedListener =
        object : ConfigurationController.ConfigurationListener {
            override fun onConfigChanged(newConfig: Configuration?) {
                updateSensorLocation()
            }
            override fun onUiModeChanged() {
                updateRippleColor()
            }
@@ -69,42 +123,49 @@ class AuthRippleController @Inject constructor(
                updateRippleColor()
            }
    }
        configurationController.addCallback(configurationChangedListener)

        commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
    inner class AuthRippleCommand : Command {
        override fun execute(pw: PrintWriter, args: List<String>) {
            if (args.isEmpty()) {
                invalidCommand(pw)
            } else {
                when (args[0]) {
                    "fingerprint" -> {
                        pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
                        showRipple(BiometricSourceType.FINGERPRINT)
                    }

    fun setSensorLocation(x: Float, y: Float) {
        rippleView.setSensorLocation(x, y)
                    "face" -> {
                        pw.println("face ripple sensorLocation=$faceSensorLocation")
                        showRipple(BiometricSourceType.FACE)
                    }

    fun setViewHost(viewHost: View) {
        // Add the ripple view to its host layout
        viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
            override fun onViewDetachedFromWindow(view: View?) {}

            override fun onViewAttachedToWindow(view: View?) {
                (viewHost as ViewGroup).addView(rippleView)
                keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
                viewHost.removeOnAttachStateChangeListener(this)
                    "custom" -> {
                        if (args.size != 3 ||
                            args[1].toFloatOrNull() == null ||
                            args[2].toFloatOrNull() == null) {
                            invalidCommand(pw)
                            return
                        }
        })

        updateRippleColor()
                        pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " +
                            args[2].toFloat())
                        mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat()))
                        showRipple()
                    }
                    else -> invalidCommand(pw)
                }

    private fun updateRippleColor() {
        rippleView.setColor(
            Utils.getColorAttr(context, android.R.attr.colorAccent).defaultColor)
            }

    inner class AuthRippleCommand : Command {
        override fun execute(pw: PrintWriter, args: List<String>) {
            rippleView.startRipple()
        }

        override fun help(pw: PrintWriter) {
            pw.println("Usage: adb shell cmd statusbar auth-ripple")
            pw.println("Usage: adb shell cmd statusbar auth-ripple <command>")
            pw.println("Available commands:")
            pw.println("  fingerprint")
            pw.println("  face")
            pw.println("  custom <x-location: int> <y-location: int>")
        }

        fun invalidCommand(pw: PrintWriter) {
            pw.println("invalid command")
            help(pw)
        }
    }
}
+37 −12
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ package com.android.systemui.biometrics

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
@@ -24,9 +25,11 @@ import android.graphics.Paint
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import android.view.animation.PathInterpolator
import com.android.internal.graphics.ColorUtils
import com.android.systemui.statusbar.charging.RippleShader

private const val RIPPLE_ANIMATION_DURATION: Long = 950
private const val RIPPLE_ANIMATION_DURATION: Long = 1533
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f

/**
@@ -36,42 +39,64 @@ private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
    private var rippleInProgress: Boolean = false
    private val rippleShader = RippleShader()
    private val defaultColor: Int = 0xffffffff.toInt()
    private val ripplePaint = Paint()

    init {
        rippleShader.color = defaultColor
        rippleShader.color = 0xffffffff.toInt() // default color
        rippleShader.progress = 0f
        rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
        ripplePaint.shader = rippleShader
        visibility = View.GONE
        visibility = GONE
    }

    fun setSensorLocation(x: Float, y: Float) {
        rippleShader.origin = PointF(x, y)
        rippleShader.radius = maxOf(x, y, width - x, height - y).toFloat()
    fun setSensorLocation(location: PointF) {
        rippleShader.origin = location
        rippleShader.radius = maxOf(location.x, location.y, width - location.x, height - location.y)
            .toFloat()
    }

    fun startRipple() {
    fun startRipple(onAnimationEnd: Runnable?) {
        if (rippleInProgress) {
            return // Ignore if ripple effect is already playing
        }

        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.interpolator = PathInterpolator(0.4f, 0f, 0f, 1f)
        animator.duration = RIPPLE_ANIMATION_DURATION
        animator.addUpdateListener { animator ->
            val now = animator.currentPlayTime
            rippleShader.progress = animator.animatedValue as Float
            rippleShader.time = now.toFloat()
            rippleShader.distortionStrength = 1 - rippleShader.progress
            invalidate()
        }
        val alphaInAnimator = ValueAnimator.ofInt(0, 127)
        alphaInAnimator.duration = 167
        alphaInAnimator.addUpdateListener { alphaInAnimator ->
            rippleShader.color = ColorUtils.setAlphaComponent(rippleShader.color,
                alphaInAnimator.animatedValue as Int)
            invalidate()
        }
        animator.addListener(object : AnimatorListenerAdapter() {
        val alphaOutAnimator = ValueAnimator.ofInt(127, 0)
        alphaOutAnimator.startDelay = 417
        alphaOutAnimator.duration = 1116
        alphaOutAnimator.addUpdateListener { alphaOutAnimator ->
            rippleShader.color = ColorUtils.setAlphaComponent(rippleShader.color,
                alphaOutAnimator.animatedValue as Int)
            invalidate()
        }

        val animatorSet = AnimatorSet()
        animatorSet.playTogether(animator, alphaInAnimator, alphaOutAnimator)
        animatorSet.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                onAnimationEnd?.run()
                rippleInProgress = false
                visibility = View.GONE
                visibility = GONE
            }
        })
        animator.start()
        visibility = View.VISIBLE
        animatorSet.start()
        visibility = VISIBLE
        rippleInProgress = true
    }

Loading