diff options
| author | 2021-03-23 22:14:27 +0000 | |
|---|---|---|
| committer | 2021-03-23 22:14:27 +0000 | |
| commit | f62fa10c22aec19bc596c8d71e76e8cb83cd4ea0 (patch) | |
| tree | 63af0547af0f655dbe24d83cfbb305748afc13e1 | |
| parent | 3f55bdd6bd68461311cf74a0dd4bb1048c3df418 (diff) | |
| parent | a55b15e6776fa8db7e60fc24d493e737f0378944 (diff) | |
Merge "Add ripple effect to udfps animation" into sc-dev
5 files changed, 322 insertions, 2 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt new file mode 100644 index 000000000000..a1149fd2a447 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -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") + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt new file mode 100644 index 000000000000..2e321338999c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -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) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 98b3fe46ff57..078ec9fdfd1c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -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 diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt new file mode 100644 index 000000000000..02ba304f8c37 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -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) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 3f1a927dd3ed..d3694ddedae4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -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(); |