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

Commit f62fa10c authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "Add ripple effect to udfps animation" into sc-dev

parents 3f55bdd6 a55b15e6
Loading
Loading
Loading
Loading
+110 −0
Original line number Diff line number Diff line
/*
 * 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.content.Context
import android.hardware.biometrics.BiometricSourceType
import android.view.View
import android.view.ViewGroup
import com.android.internal.annotations.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.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.ConfigurationController
import java.io.PrintWriter
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
class AuthRippleController @Inject constructor(
    commandRegistry: CommandRegistry,
    configurationController: ConfigurationController,
    private val context: Context,
    private val keyguardUpdateMonitor: KeyguardUpdateMonitor
) {
    @VisibleForTesting
    var rippleView: AuthRippleView = AuthRippleView(context, attrs = null)

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

    init {
        val configurationChangedListener = object : ConfigurationController.ConfigurationListener {
            override fun onUiModeChanged() {
                updateRippleColor()
            }
            override fun onThemeChanged() {
                updateRippleColor()
            }
            override fun onOverlayChanged() {
                updateRippleColor()
            }
        }
        configurationController.addCallback(configurationChangedListener)

        commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
    }

    fun setSensorLocation(x: Float, y: Float) {
        rippleView.setSensorLocation(x, y)
    }

    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)
            }
        })

        updateRippleColor()
    }

    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")
        }
    }
}
+87 −0
Original line number Diff line number Diff line
/*
 * 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.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import com.android.systemui.statusbar.charging.RippleShader

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

/**
 * Expanding ripple effect on the transition from biometric authentication success to showing
 * launcher.
 */
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.progress = 0f
        rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
        ripplePaint.shader = rippleShader
        visibility = View.GONE
    }

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

    fun startRipple() {
        if (rippleInProgress) {
            return // Ignore if ripple effect is already playing
        }
        val animator = ValueAnimator.ofFloat(0f, 1f)
        animator.duration = RIPPLE_ANIMATION_DURATION
        animator.addUpdateListener { animator ->
            val now = animator.currentPlayTime
            val phase = now / 30000f
            rippleShader.progress = animator.animatedValue as Float
            rippleShader.noisePhase = phase
            invalidate()
        }
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                rippleInProgress = false
                visibility = View.GONE
            }
        })
        animator.start()
        visibility = View.VISIBLE
        rippleInProgress = true
    }

    fun setColor(color: Int) {
        rippleShader.color = color
    }

    override fun onDraw(canvas: Canvas?) {
        // draw over the entire screen
        canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), ripplePaint)
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -87,6 +87,7 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
    @NonNull private final StatusBarStateController mStatusBarStateController;
    @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager;
    @NonNull private final DumpManager mDumpManager;
    @NonNull private final AuthRippleController mAuthRippleController;
    // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
    // sensors, this, in addition to a lot of the code here, will be updated.
    @VisibleForTesting final FingerprintSensorPropertiesInternal mSensorProps;
@@ -305,7 +306,8 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
            @Main DelayableExecutor fgExecutor,
            @NonNull StatusBar statusBar,
            @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
            @NonNull DumpManager dumpManager) {
            @NonNull DumpManager dumpManager,
            @NonNull AuthRippleController authRippleController) {
        mContext = context;
        mInflater = inflater;
        // The fingerprint manager is queried for UDFPS before this class is constructed, so the
@@ -317,6 +319,7 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
        mStatusBarStateController = statusBarStateController;
        mKeyguardViewManager = statusBarKeyguardViewManager;
        mDumpManager = dumpManager;
        mAuthRippleController = authRippleController;

        mSensorProps = findFirstUdfps();
        // At least one UDFPS sensor exists
@@ -343,6 +346,10 @@ public class UdfpsController implements DozeReceiver, HbmCallback {
        final IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        context.registerReceiver(mBroadcastReceiver, filter);

        mAuthRippleController.setViewHost(mStatusBar.getNotificationShadeWindowView());
        mAuthRippleController.setSensorLocation(getSensorLocation().centerX(),
                getSensorLocation().centerY());
    }

    @Nullable
+113 −0
Original line number Diff line number Diff line
/*
 * 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.hardware.biometrics.BiometricSourceType
import android.testing.AndroidTestingRunner
import android.view.View
import android.view.ViewGroup
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.ConfigurationController
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
class AuthRippleControllerTest : SysuiTestCase() {
    private lateinit var controller: AuthRippleController
    @Mock private lateinit var commandRegistry: CommandRegistry
    @Mock private lateinit var configurationController: ConfigurationController
    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock private lateinit var rippleView: AuthRippleView
    @Mock private lateinit var viewHost: ViewGroup

    @Before

    fun setUp() {
        MockitoAnnotations.initMocks(this)
        controller = AuthRippleController(
            commandRegistry, configurationController, context, keyguardUpdateMonitor)
        controller.rippleView = rippleView // Replace the real ripple view with a mock instance
        controller.setViewHost(viewHost)
    }

    @Test
    fun testAddRippleView() {
        val listenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
        verify(viewHost).addOnAttachStateChangeListener(listenerCaptor.capture())

        // Fake attach to window
        listenerCaptor.value.onViewAttachedToWindow(viewHost)
        verify(viewHost).addView(rippleView)
    }

    @Test
    fun testTriggerRipple() {
        // Fake attach to window
        val listenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
        verify(viewHost).addOnAttachStateChangeListener(listenerCaptor.capture())
        listenerCaptor.value.onViewAttachedToWindow(viewHost)

        val captor = ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback::class.java)
        verify(keyguardUpdateMonitor).registerCallback(captor.capture())

        captor.value.onBiometricAuthenticated(
            0 /* userId */,
            BiometricSourceType.FACE /* type */,
            false /* isStrongBiometric */)
        verify(rippleView, never()).startRipple()

        captor.value.onBiometricAuthenticated(
            0 /* userId */,
            BiometricSourceType.FINGERPRINT /* type */,
            false /* isStrongBiometric */)
        verify(rippleView).startRipple()
    }

    @Test
    fun testUpdateRippleColor() {
        val captor = ArgumentCaptor
            .forClass(ConfigurationController.ConfigurationListener::class.java)
        verify(configurationController).addCallback(captor.capture())

        reset(rippleView)
        captor.value.onThemeChanged()
        verify(rippleView).setColor(ArgumentMatchers.anyInt())

        reset(rippleView)
        captor.value.onUiModeChanged()
        verify(rippleView).setColor(ArgumentMatchers.anyInt())
    }

    @Test
    fun testForwardsSensorLocation() {
        controller.setSensorLocation(5f, 5f)
        verify(rippleView).setSensorLocation(5f, 5f)
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -98,6 +98,8 @@ public class UdfpsControllerTest extends SysuiTestCase {
    @Mock
    private DumpManager mDumpManager;
    @Mock
    private AuthRippleController mAuthRippleController;
    @Mock
    private IUdfpsOverlayControllerCallback mUdfpsOverlayControllerCallback;

    private FakeExecutor mFgExecutor;
@@ -148,7 +150,8 @@ public class UdfpsControllerTest extends SysuiTestCase {
                mFgExecutor,
                mStatusBar,
                mStatusBarKeyguardViewManager,
                mDumpManager);
                mDumpManager,
                mAuthRippleController);
        verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
        mOverlayController = mOverlayCaptor.getValue();