Merge "Add ripple effect to udfps animation" into sc-dev
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user