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

This commit is contained in:
Beverly Tai
2021-03-23 22:14:27 +00:00
committed by Android (Google) Code Review
5 changed files with 322 additions and 2 deletions

View File

@@ -0,0 +1,110 @@
/*
* 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")
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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)
}
}

View File

@@ -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

View File

@@ -0,0 +1,113 @@
/*
* 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)
}
}

View File

@@ -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();