Add dwell-ripple am: 3ad5b8bb1e am: f26e310e0d am: 35cf2aa362

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

Change-Id: I53d07736d17128ad383315e017411e10a0375ec8
This commit is contained in:
Beverly
2021-09-11 03:19:28 +00:00
committed by Automerger Merge Worker
5 changed files with 368 additions and 30 deletions

View File

@@ -20,6 +20,7 @@ import android.content.Context
import android.content.res.Configuration
import android.graphics.PointF
import android.hardware.biometrics.BiometricSourceType
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.ViewController
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
/***
* Controls the ripple effect that shows when authentication is successful.
@@ -54,12 +56,18 @@ class AuthRippleController @Inject constructor(
private val notificationShadeWindowController: NotificationShadeWindowController,
private val bypassController: KeyguardBypassController,
private val biometricUnlockController: BiometricUnlockController,
private val udfpsControllerProvider: Provider<UdfpsController>,
rippleView: AuthRippleView?
) : ViewController<AuthRippleView>(rippleView) {
var fingerprintSensorLocation: PointF? = null
private var faceSensorLocation: PointF? = null
private var circleReveal: LightRevealEffect? = null
private var udfpsController: UdfpsController? = null
private var dwellScale = 2f
private var expandedDwellScale = 2.5f
private var udfpsRadius: Float = -1f
override fun onInit() {
mView.setAlphaInDuration(sysuiContext.resources.getInteger(
R.integer.auth_ripple_alpha_in_duration).toLong())
@@ -67,9 +75,11 @@ class AuthRippleController @Inject constructor(
@VisibleForTesting
public override fun onViewAttached() {
authController.addCallback(authControllerCallback)
updateRippleColor()
updateSensorLocation()
authController.addCallback(authControllerCallback)
updateUdfpsDependentParams()
udfpsController?.addCallback(udfpsControllerCallback)
configurationController.addCallback(configurationChangedListener)
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
@@ -77,6 +87,7 @@ class AuthRippleController @Inject constructor(
@VisibleForTesting
public override fun onViewDetached() {
udfpsController?.removeCallback(udfpsControllerCallback)
authController.removeCallback(authControllerCallback)
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
configurationController.removeCallback(configurationChangedListener)
@@ -94,18 +105,18 @@ class AuthRippleController @Inject constructor(
if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
fingerprintSensorLocation != null) {
mView.setSensorLocation(fingerprintSensorLocation!!)
showRipple()
showUnlockedRipple()
} else if (biometricSourceType == BiometricSourceType.FACE &&
faceSensorLocation != null) {
if (!bypassController.canBypass()) {
return
}
mView.setSensorLocation(faceSensorLocation!!)
showRipple()
showUnlockedRipple()
}
}
private fun showRipple() {
private fun showUnlockedRipple() {
notificationShadeWindowController.setForcePluginOpen(true, this)
val biometricUnlockMode = biometricUnlockController.mode
val useCircleReveal = circleReveal != null &&
@@ -117,7 +128,7 @@ class AuthRippleController @Inject constructor(
lightRevealScrim?.revealEffect = circleReveal!!
}
mView.startRipple(
mView.startUnlockedRipple(
/* end runnable */
Runnable {
notificationShadeWindowController.setForcePluginOpen(false, this)
@@ -152,7 +163,7 @@ class AuthRippleController @Inject constructor(
Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor)
}
val keyguardUpdateMonitorCallback =
private val keyguardUpdateMonitorCallback =
object : KeyguardUpdateMonitorCallback() {
override fun onBiometricAuthenticated(
userId: Int,
@@ -161,9 +172,13 @@ class AuthRippleController @Inject constructor(
) {
showRipple(biometricSourceType)
}
override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
mView.retractRipple()
}
}
val configurationChangedListener =
private val configurationChangedListener =
object : ConfigurationController.ConfigurationListener {
override fun onConfigChanged(newConfig: Configuration?) {
updateSensorLocation()
@@ -179,14 +194,97 @@ class AuthRippleController @Inject constructor(
}
}
private val authControllerCallback = AuthController.Callback { updateSensorLocation() }
private val udfpsControllerCallback =
object : UdfpsController.Callback {
override fun onFingerDown() {
if (fingerprintSensorLocation == null) {
Log.e("AuthRipple", "fingerprintSensorLocation=null onFingerDown. " +
"Skip showing dwell ripple")
return
}
mView.setSensorLocation(fingerprintSensorLocation!!)
mView.startDwellRipple(
/* startRadius */ udfpsRadius,
/* endRadius */ udfpsRadius * dwellScale,
/* expandedRadius */ udfpsRadius * expandedDwellScale)
}
override fun onFingerUp() {
mView.retractRipple()
}
}
private val authControllerCallback = AuthController.Callback {
updateSensorLocation()
updateUdfpsDependentParams()
}
private fun updateUdfpsDependentParams() {
authController.udfpsProps?.let {
if (it.size > 0) {
udfpsRadius = it[0].sensorRadius.toFloat()
udfpsController = udfpsControllerProvider.get()
if (mView.isAttachedToWindow) {
udfpsController?.addCallback(udfpsControllerCallback)
}
}
}
}
inner class AuthRippleCommand : Command {
fun printDwellInfo(pw: PrintWriter) {
pw.println("dwell ripple: " +
"\n\tsensorLocation=$fingerprintSensorLocation" +
"\n\tdwellScale=$dwellScale" +
"\n\tdwellAlpha=${mView.dwellAlpha}, " +
"duration=${mView.dwellAlphaDuration}" +
"\n\tdwellExpand=$expandedDwellScale" +
"\n\t(crash systemui to reset to default)")
}
override fun execute(pw: PrintWriter, args: List<String>) {
if (args.isEmpty()) {
invalidCommand(pw)
} else {
when (args[0]) {
"dwellScale" -> {
if (args.size > 1 && args[1].toFloatOrNull() != null) {
dwellScale = args[1].toFloat()
printDwellInfo(pw)
} else {
pw.println("expected float argument <dwellScale>")
}
}
"dwellAlpha" -> {
if (args.size > 2 && args[1].toFloatOrNull() != null &&
args[2].toLongOrNull() != null) {
mView.dwellAlpha = args[1].toFloat()
if (args[2].toFloat() > 200L) {
pw.println("alpha animation duration must be less than 200ms.")
}
mView.dwellAlphaDuration = kotlin.math.min(args[2].toLong(), 200L)
printDwellInfo(pw)
} else {
pw.println("expected two float arguments:" +
" <dwellAlpha> <dwellAlphaDuration>")
}
}
"dwellExpand" -> {
if (args.size > 1 && args[1].toFloatOrNull() != null) {
val expandedScale = args[1].toFloat()
if (expandedScale <= dwellScale) {
pw.println("invalid expandedScale. must be greater than " +
"dwellScale=$dwellScale, but given $expandedScale")
} else {
expandedDwellScale = expandedScale
}
printDwellInfo(pw)
} else {
pw.println("expected float argument <expandedScale>")
}
}
"fingerprint" -> {
pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
showRipple(BiometricSourceType.FINGERPRINT)
@@ -205,7 +303,7 @@ class AuthRippleController @Inject constructor(
pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " +
args[2].toFloat())
mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat()))
showRipple()
showUnlockedRipple()
}
else -> invalidCommand(pw)
}
@@ -215,6 +313,9 @@ class AuthRippleController @Inject constructor(
override fun help(pw: PrintWriter) {
pw.println("Usage: adb shell cmd statusbar auth-ripple <command>")
pw.println("Available commands:")
pw.println(" dwellScale <200ms_scale: float>")
pw.println(" dwellAlpha <alpha: float> <duration : long>")
pw.println(" dwellExpand <expanded_scale: float>")
pw.println(" fingerprint")
pw.println(" face")
pw.println(" custom <x-location: int> <y-location: int>")

View File

@@ -26,7 +26,9 @@ import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import android.view.animation.PathInterpolator
import com.android.internal.R.attr.interpolator
import com.android.internal.graphics.ColorUtils
import com.android.systemui.animation.Interpolators
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.charging.RippleShader
@@ -34,15 +36,28 @@ private const val RIPPLE_ANIMATION_DURATION: Long = 1533
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
/**
* Expanding ripple effect on the transition from biometric authentication success to showing
* Expanding ripple effect
* - startUnlockedRipple for the transition from biometric authentication success to showing
* launcher.
* - startDwellRipple for the ripple expansion out when the user has their finger down on the UDFPS
* sensor area
* - retractRipple for the ripple animation inwards to signal a failure
*/
class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val retractInterpolator = PathInterpolator(.05f, .93f, .1f, 1f)
private val dwellPulseDuration = 200L
var dwellAlphaDuration = dwellPulseDuration
private val dwellExpandDuration = 1200L - dwellPulseDuration
private val retractDuration = 400L
var dwellAlpha: Float = .5f
private var alphaInDuration: Long = 0
private var rippleInProgress: Boolean = false
private var unlockedRippleInProgress: Boolean = false
private val rippleShader = RippleShader()
private val ripplePaint = Paint()
private var radius: Float = 0.0f
private var retractAnimator: Animator? = null
private var dwellPulseOutAnimator: Animator? = null
private var radius: Float = 0f
set(value) {
rippleShader.radius = value
field = value
@@ -63,21 +78,182 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
fun setSensorLocation(location: PointF) {
origin = location
radius = maxOf(location.x, location.y, width - location.x, height - location.y)
.toFloat()
radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
}
fun setAlphaInDuration(duration: Long) {
alphaInDuration = duration
}
fun startRipple(onAnimationEnd: Runnable?, lightReveal: LightRevealScrim?) {
if (rippleInProgress) {
/**
* Animate ripple inwards back to radius 0
*/
fun retractRipple() {
if (retractAnimator?.isRunning == true) {
return // let the animation finish
}
if (dwellPulseOutAnimator?.isRunning == true) {
val retractRippleAnimator = ValueAnimator.ofFloat(rippleShader.progress, 0f)
.apply {
interpolator = retractInterpolator
duration = retractDuration
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val retractAlphaAnimator = ValueAnimator.ofInt(255, 0).apply {
interpolator = Interpolators.LINEAR
duration = retractDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
retractAnimator = AnimatorSet().apply {
playTogether(retractRippleAnimator, retractAlphaAnimator)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
dwellPulseOutAnimator?.cancel()
rippleShader.shouldFadeOutRipple = false
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
visibility = GONE
resetRippleAlpha()
}
})
start()
}
}
}
/**
* Ripple that moves animates from an outer ripple ring of
* startRadius => endRadius => expandedRadius
*/
fun startDwellRipple(startRadius: Float, endRadius: Float, expandedRadius: Float) {
if (unlockedRippleInProgress || dwellPulseOutAnimator?.isRunning == true) {
return
}
// we divide by 4 because the desired startRadius and endRadius is for the ripple's outer
// ring see RippleShader
val startDwellProgress = startRadius / radius / 4f
val endInitialDwellProgress = endRadius / radius / 4f
val endExpandDwellProgress = expandedRadius / radius / 4f
val pulseOutEndAlpha = (255 * dwellAlpha).toInt()
val expandDwellEndAlpha = kotlin.math.min((255 * (dwellAlpha + .25f)).toInt(), 255)
val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(startDwellProgress,
endInitialDwellProgress).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = dwellPulseDuration
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val dwellPulseOutAlphaAnimator = ValueAnimator.ofInt(0, pulseOutEndAlpha).apply {
interpolator = Interpolators.LINEAR
duration = dwellAlphaDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
// slowly animate outwards until we receive a call to retractRipple or startUnlockedRipple
val expandDwellRippleAnimator = ValueAnimator.ofFloat(endInitialDwellProgress,
endExpandDwellProgress).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = dwellExpandDuration
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val expandDwellAlphaAnimator = ValueAnimator.ofInt(pulseOutEndAlpha, expandDwellEndAlpha)
.apply {
interpolator = Interpolators.LINEAR
duration = dwellExpandDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
val initialDwellPulseOutAnimator = AnimatorSet().apply {
playTogether(dwellPulseOutRippleAnimator, dwellPulseOutAlphaAnimator)
}
val expandDwellAnimator = AnimatorSet().apply {
playTogether(expandDwellRippleAnimator, expandDwellAlphaAnimator)
}
dwellPulseOutAnimator = AnimatorSet().apply {
playSequentially(
initialDwellPulseOutAnimator,
expandDwellAnimator
)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
retractAnimator?.cancel()
rippleShader.shouldFadeOutRipple = false
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
visibility = GONE
resetRippleAlpha()
}
})
start()
}
}
/**
* Ripple that bursts outwards from the position of the sensor to the edges of the screen
*/
fun startUnlockedRipple(onAnimationEnd: Runnable?, lightReveal: LightRevealScrim?) {
if (unlockedRippleInProgress) {
return // Ignore if ripple effect is already playing
}
val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
interpolator = PathInterpolator(0f, 0f, .2f, 1f)
var rippleStart = 0f
var alphaDuration = alphaInDuration
if (dwellPulseOutAnimator?.isRunning == true || retractAnimator?.isRunning == true) {
rippleStart = rippleShader.progress
alphaDuration = 0
dwellPulseOutAnimator?.cancel()
retractAnimator?.cancel()
}
val rippleAnimator = ValueAnimator.ofFloat(rippleStart, 1f).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = RIPPLE_ANIMATION_DURATION
addUpdateListener { animator ->
val now = animator.currentPlayTime
@@ -97,7 +273,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
}
val alphaInAnimator = ValueAnimator.ofInt(0, 255).apply {
duration = alphaInDuration
duration = alphaDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
@@ -115,13 +291,14 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
rippleInProgress = true
unlockedRippleInProgress = true
rippleShader.shouldFadeOutRipple = true
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
onAnimationEnd?.run()
rippleInProgress = false
unlockedRippleInProgress = false
visibility = GONE
}
})
@@ -129,8 +306,16 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
animatorSet.start()
}
fun resetRippleAlpha() {
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
255
)
}
fun setColor(color: Int) {
rippleShader.color = color
resetRippleAlpha()
}
override fun onDraw(canvas: Canvas?) {

View File

@@ -77,7 +77,9 @@ import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.Execution;
import com.android.systemui.util.time.SystemClock;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
@@ -158,6 +160,7 @@ public class UdfpsController implements DozeReceiver {
private Runnable mAodInterruptRunnable;
private boolean mOnFingerDown;
private boolean mAttemptedToDismissKeyguard;
private Set<Callback> mCallbacks = new HashSet<>();
@VisibleForTesting
public static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES =
@@ -876,6 +879,20 @@ public class UdfpsController implements DozeReceiver {
}
}
/**
* Add a callback for fingerUp and fingerDown events
*/
public void addCallback(Callback cb) {
mCallbacks.add(cb);
}
/**
* Remove callback
*/
public void removeCallback(Callback cb) {
mCallbacks.remove(cb);
}
/**
* Cancel updfs scan affordances - ability to hide the HbmSurfaceView (white circle) before
* user explicitly lifts their finger. Generally, this should be called whenever udfps fails
@@ -930,6 +947,10 @@ public class UdfpsController implements DozeReceiver {
mFingerprintManager.onUiReady(mSensorProps.sensorId);
Trace.endAsyncSection("UdfpsController.e2e.startIllumination", 0);
});
for (Callback cb : mCallbacks) {
cb.onFingerDown();
}
}
private void onFingerUp() {
@@ -942,6 +963,9 @@ public class UdfpsController implements DozeReceiver {
}
if (mOnFingerDown) {
mFingerprintManager.onPointerUp(mSensorProps.sensorId);
for (Callback cb : mCallbacks) {
cb.onFingerUp();
}
}
mOnFingerDown = false;
if (mView.isIlluminationRequested()) {
@@ -962,4 +986,19 @@ public class UdfpsController implements DozeReceiver {
mView.setOnTouchListener(mOnTouchListener);
}
}
/**
* Callback for fingerUp and fingerDown events.
*/
public interface Callback {
/**
* Called onFingerUp events. Will only be called if the finger was previously down.
*/
void onFingerUp();
/**
* Called onFingerDown events.
*/
void onFingerDown();
}
}

View File

@@ -147,8 +147,12 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) {
val fadeIn = subProgress(0f, 0.1f, value)
val fadeOutNoise = subProgress(0.4f, 1f, value)
val fadeOutRipple = subProgress(0.3f, 1f, value)
val fadeCircle = subProgress(0f, 0.2f, value)
var fadeOutRipple = 0f
var fadeCircle = 0f
if (shouldFadeOutRipple) {
fadeCircle = subProgress(0f, 0.2f, value)
fadeOutRipple = subProgress(0.3f, 1f, value)
}
setUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
setUniform("in_fadeCircle", 1 - fadeCircle)
setUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
@@ -200,4 +204,6 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) {
field = value
setUniform("in_pixelDensity", value)
}
var shouldFadeOutRipple: Boolean = true
}

View File

@@ -42,6 +42,8 @@ import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import javax.inject.Provider
@SmallTest
@RunWith(AndroidTestingRunner::class)
class AuthRippleControllerTest : SysuiTestCase() {
@@ -55,10 +57,14 @@ class AuthRippleControllerTest : SysuiTestCase() {
@Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
@Mock private lateinit var bypassController: KeyguardBypassController
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
@Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController>
@Mock private lateinit var udfpsController: UdfpsController
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
`when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
controller = AuthRippleController(
statusBar,
context,
@@ -69,6 +75,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
notificationShadeWindowController,
bypassController,
biometricUnlockController,
udfpsControllerProvider,
rippleView
)
controller.init()
@@ -93,7 +100,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
// THEN update sensor location and show ripple
verify(rippleView).setSensorLocation(fpsLocation)
verify(rippleView).startRipple(any(), any())
verify(rippleView).startUnlockedRipple(any(), any())
}
@Test
@@ -114,7 +121,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
false /* isStrongBiometric */)
// THEN no ripple
verify(rippleView, never()).startRipple(any(), any())
verify(rippleView, never()).startUnlockedRipple(any(), any())
}
@Test
@@ -135,7 +142,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
false /* isStrongBiometric */)
// THEN no ripple
verify(rippleView, never()).startRipple(any(), any())
verify(rippleView, never()).startUnlockedRipple(any(), any())
}
@Test
@@ -159,7 +166,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
// THEN show ripple
verify(rippleView).setSensorLocation(faceLocation)
verify(rippleView).startRipple(any(), any())
verify(rippleView).startUnlockedRipple(any(), any())
}
@Test
@@ -179,7 +186,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
false /* isStrongBiometric */)
// THEN no ripple
verify(rippleView, never()).startRipple(any(), any())
verify(rippleView, never()).startUnlockedRipple(any(), any())
}
@Test
@@ -194,7 +201,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
0 /* userId */,
BiometricSourceType.FACE /* type */,
false /* isStrongBiometric */)
verify(rippleView, never()).startRipple(any(), any())
verify(rippleView, never()).startUnlockedRipple(any(), any())
}
@Test
@@ -209,7 +216,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
0 /* userId */,
BiometricSourceType.FINGERPRINT /* type */,
false /* isStrongBiometric */)
verify(rippleView, never()).startRipple(any(), any())
verify(rippleView, never()).startUnlockedRipple(any(), any())
}
@Test