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

Commit 351d4f26 authored by Beverly Tai's avatar Beverly Tai Committed by Automerger Merge Worker
Browse files

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

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/13956352

Change-Id: I75e30d5436ffece2406c7874d82509fd72f88d5f
parents 004ad959 f62fa10c
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();