From 3a13219bac1855461858b3ed83184fd1afc2bdb3 Mon Sep 17 00:00:00 2001 From: Shan Huang Date: Sat, 13 Mar 2021 14:40:01 +0800 Subject: [PATCH] Add the charging ripple effect, currently guarded by flag_charging_ripple. The effect is drawn in a full screen view in StatusBar, using a shader adapted from graphics/java/android/graphics/drawable/RippleShader.java. It plays whenever a phone is plugged in and wired charging begins. Next steps: - Add distortion effect to the first half of the animation. - More API to support the UDFPS reveal effect. Videos: https://drive.google.com/drive/folders/1c_QduSrELp4-Y4oJdHIllnypQkguDd-x?usp=sharing Test: atest SystemUITests Test: Manually Bug: 182719493 Change-Id: If8885022a906f617332ca21132c3194ab26c09a4 --- packages/SystemUI/res/values/flags.xml | 2 + .../systemui/statusbar/FeatureFlags.java | 4 + .../statusbar/charging/ChargingRippleView.kt | 86 ++++++++++ .../statusbar/charging/RippleShader.kt | 147 ++++++++++++++++++ .../charging/WiredChargingRippleController.kt | 137 ++++++++++++++++ .../systemui/statusbar/phone/StatusBar.java | 7 + .../phone/dagger/StatusBarPhoneModule.java | 3 + .../WiredChargingRippleControllerTest.kt | 115 ++++++++++++++ .../statusbar/phone/StatusBarTest.java | 3 + 9 files changed, 504 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml index 8cd5757247e21..e7dc1968b6f14 100644 --- a/packages/SystemUI/res/values/flags.xml +++ b/packages/SystemUI/res/values/flags.xml @@ -48,4 +48,6 @@ false false + + false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java index c8e0d60ce304a..5de2aabe8de05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java @@ -94,4 +94,8 @@ public class FeatureFlags { public boolean isAlarmTileAvailable() { return mFlagReader.isEnabled(R.bool.flag_alarm_tile); } + + public boolean isChargingRippleEnabled() { + return mFlagReader.isEnabled(R.bool.flag_charging_ripple); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt new file mode 100644 index 0000000000000..6f80317e8f562 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/ChargingRippleView.kt @@ -0,0 +1,86 @@ +/* + * 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.statusbar.charging + +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 kotlin.math.max + +private const val RIPPLE_ANIMATION_DURATION: Long = 1500 +private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f + +/** + * Expanding ripple effect that shows when charging begins. + */ +class ChargingRippleView(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 + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + rippleShader.origin = PointF(measuredWidth / 2f, measuredHeight.toFloat()) + rippleShader.radius = max(measuredWidth, measuredHeight).toFloat() + super.onLayout(changed, left, top, right, bottom) + } + + 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?) { + canvas?.drawRect(0f, 0f, width.toFloat(), height.toFloat(), ripplePaint) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt new file mode 100644 index 0000000000000..5547c1e78cc4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/RippleShader.kt @@ -0,0 +1,147 @@ +/* + * 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.statusbar.charging + +import android.graphics.Color +import android.graphics.PointF +import android.graphics.RuntimeShader + +/** + * Shader class that renders an expanding charging ripple effect. A charging ripple contains + * three elements: + * 1. an expanding filled circle that appears in the beginning and quickly fades away + * 2. an expanding ring that appears throughout the effect + * 3. an expanding ring-shaped area that reveals noise over #2. + * + * Modeled after frameworks/base/graphics/java/android/graphics/drawable/RippleShader.java. + */ +class RippleShader internal constructor() : RuntimeShader(SHADER, false) { + companion object { + private const val SHADER_UNIFORMS = """uniform vec2 in_origin; + uniform float in_progress; + uniform float in_maxRadius; + uniform float in_noisePhase; + uniform vec4 in_color; + uniform float in_sparkle_strength;""" + private const val SHADER_LIB = """float triangleNoise(vec2 n) { + n = fract(n * vec2(5.3987, 5.4421)); + n += dot(n.yx, n.xy + vec2(21.5351, 14.3137)); + float xy = n.x * n.y; + return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0; + } + const float PI = 3.1415926535897932384626; + + float threshold(float v, float l, float h) { + return step(l, v) * (1.0 - step(h, v)); + } + + float sparkles(vec2 uv, float t) { + float n = triangleNoise(uv); + float s = 0.0; + for (float i = 0; i < 4; i += 1) { + float l = i * 0.25; + float h = l + 0.025; + float o = abs(sin(0.1 * PI * (t + i))); + s += threshold(n + o, l, h); + } + return saturate(s); + } + + float softCircle(vec2 uv, vec2 xy, float radius, float blur) { + float blurHalf = blur * 0.5; + float d = distance(uv, xy); + return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius); + } + + float softRing(vec2 uv, vec2 xy, float radius, float blur) { + float thickness = 0.4; + float circle_outer = softCircle(uv, xy, + radius + thickness * radius * 0.5, blur); + float circle_inner = softCircle(uv, xy, + radius - thickness * radius * 0.5, blur); + return circle_outer - circle_inner; + } + + float subProgress(float start, float end, float progress) { + float sub = clamp(progress, start, end); + return (sub - start) / (end - start); + } + + float smoothstop2(float t) { + return 1 - (1 - t) * (1 - t); + }""" + private const val SHADER_MAIN = """vec4 main(vec2 p) { + float fadeIn = subProgress(0., 0.1, in_progress); + float fadeOutNoise = subProgress(0.8, 1., in_progress); + float fadeOutRipple = subProgress(0.7, 1., in_progress); + float fadeCircle = subProgress(0., 0.5, in_progress); + float radius = smoothstop2(in_progress) * in_maxRadius; + float sparkleRing = softRing(p, in_origin, radius, 0.5); + float sparkleAlpha = min(fadeIn, 1. - fadeOutNoise); + float sparkle = sparkles(p, in_noisePhase) * sparkleRing * sparkleAlpha; + float circle = softCircle(p, in_origin, radius * 1.2, 0.5) + * (1 - fadeCircle); + float fadeRipple = min(fadeIn, 1.-fadeOutRipple); + float rippleAlpha = softRing(p, in_origin, radius, 0.5) + * fadeRipple * in_color.a; + vec4 ripple = in_color * max(circle, rippleAlpha) * 0.4; + return mix(ripple, vec4(sparkle), sparkle * in_sparkle_strength); + }""" + private const val SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN + } + + /** + * Maximum radius of the ripple. + */ + var radius: Float = 0.0f + set(value) { setUniform("in_maxRadius", value) } + + /** + * Origin coordinate of the ripple. + */ + var origin: PointF = PointF() + set(value) { setUniform("in_origin", floatArrayOf(value.x, value.y)) } + + /** + * Progress of the ripple. Float value between [0, 1]. + */ + var progress: Float = 0.0f + set(value) { setUniform("in_progress", value) } + + /** + * Continuous offset used as noise phase. + */ + var noisePhase: Float = 0.0f + set(value) { setUniform("in_noisePhase", value) } + + /** + * A hex value representing the ripple color, in the format of ARGB + */ + var color: Int = 0xffffff.toInt() + set(value) { + val color = Color.valueOf(value) + setUniform("in_color", floatArrayOf(color.red(), + color.green(), color.blue(), color.alpha())) + } + + /** + * Noise sparkle intensity. Expected value between [0, 1]. The sparkle is white, and thus + * with strength 0 it's transparent, leaving the ripple fully smooth, while with strength 1 + * it's opaque white and looks the most grainy. + */ + var sparkleStrength: Float = 0.0f + set(value) { setUniform("in_sparkle_strength", value) } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt new file mode 100644 index 0000000000000..b567ad4ccfc23 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt @@ -0,0 +1,137 @@ +/* + * 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.statusbar.charging + +import android.content.Context +import android.content.res.Configuration +import android.util.DisplayMetrics +import android.view.View +import android.view.ViewGroupOverlay +import com.android.internal.annotations.VisibleForTesting +import com.android.settingslib.Utils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.FeatureFlags +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.KeyguardStateController +import java.io.PrintWriter +import javax.inject.Inject + +/*** + * Controls the ripple effect that shows when wired charging begins. + * The ripple uses the accent color of the current theme. + */ +@SysUISingleton +class WiredChargingRippleController @Inject constructor( + commandRegistry: CommandRegistry, + batteryController: BatteryController, + configurationController: ConfigurationController, + featureFlags: FeatureFlags, + private val context: Context, + private val keyguardStateController: KeyguardStateController +) { + private var pluggedIn: Boolean? = null + private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled + @VisibleForTesting + var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null) + + init { + val batteryStateChangeCallback = object : BatteryController.BatteryStateChangeCallback { + override fun onBatteryLevelChanged( + level: Int, + nowPluggedIn: Boolean, + charging: Boolean + ) { + if (!rippleEnabled) { + return + } + val wasPluggedIn = pluggedIn + pluggedIn = nowPluggedIn + // Only triggers when the keyguard is active and the device is just plugged in. + if (wasPluggedIn == false && nowPluggedIn && keyguardStateController.isShowing) { + rippleView.startRipple() + } + } + } + batteryController.addCallback(batteryStateChangeCallback) + + val configurationChangedListener = object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + updateRippleColor() + } + override fun onThemeChanged() { + updateRippleColor() + } + override fun onOverlayChanged() { + updateRippleColor() + } + override fun onConfigChanged(newConfig: Configuration?) { + layoutRippleView() + } + } + configurationController.addCallback(configurationChangedListener) + + commandRegistry.registerCommand("charging-ripple") { ChargingRippleCommand() } + } + + fun setViewHost(viewHost: View) { + // Add the ripple view as an overlay of the root view so that it always + // shows on top. + viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewDetachedFromWindow(view: View?) {} + + override fun onViewAttachedToWindow(view: View?) { + (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay).add(rippleView) + layoutRippleView() + viewHost.removeOnAttachStateChangeListener(this) + } + }) + + updateRippleColor() + } + + private fun layoutRippleView() { + // Overlays are not auto measured and laid out so we do it manually here. + val displayMetrics = DisplayMetrics() + context.display.getRealMetrics(displayMetrics) + val width = displayMetrics.widthPixels + val height = displayMetrics.heightPixels + if (width != rippleView.width || height != rippleView.height) { + rippleView.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)) + rippleView.layout(0, 0, width, height) + } + } + + private fun updateRippleColor() { + rippleView.setColor( + Utils.getColorAttr(context, android.R.attr.colorAccent).defaultColor) + } + + inner class ChargingRippleCommand : Command { + override fun execute(pw: PrintWriter, args: List) { + rippleView.startRipple() + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar charging-ripple") + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 117921dd860cf..14c114413a258 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -201,6 +201,8 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.charging.ChargingRippleView; +import com.android.systemui.statusbar.charging.WiredChargingRippleController; import com.android.systemui.statusbar.notification.ActivityLaunchAnimator; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -376,6 +378,8 @@ public class StatusBar extends SystemUI implements DemoMode, private boolean mWakeUpComingFromTouch; private PointF mWakeUpTouchLocation; private LightRevealScrim mLightRevealScrim; + private ChargingRippleView mChargingRipple; + private WiredChargingRippleController mChargingRippleAnimationController; private PowerButtonReveal mPowerButtonReveal; private final Object mQueueLock = new Object(); @@ -765,6 +769,7 @@ public class StatusBar extends SystemUI implements DemoMode, StatusBarTouchableRegionManager statusBarTouchableRegionManager, NotificationIconAreaController notificationIconAreaController, BrightnessSlider.Factory brightnessSliderFactory, + WiredChargingRippleController chargingRippleAnimationController, FeatureFlags featureFlags) { super(context); mNotificationsController = notificationsController; @@ -843,6 +848,7 @@ public class StatusBar extends SystemUI implements DemoMode, mDemoModeController = demoModeController; mNotificationIconAreaController = notificationIconAreaController; mBrightnessSliderFactory = brightnessSliderFactory; + mChargingRippleAnimationController = chargingRippleAnimationController; mFeatureFlags = featureFlags; mExpansionChangedListeners = new ArrayList<>(); @@ -1184,6 +1190,7 @@ public class StatusBar extends SystemUI implements DemoMode, mScrimController.attachViews(scrimBehind, scrimInFront, scrimForBubble); mLightRevealScrim = mNotificationShadeWindowView.findViewById(R.id.light_reveal_scrim); + mChargingRippleAnimationController.setViewHost(mNotificationShadeWindowView); if (mFeatureFlags.useNewLockscreenAnimations() && mDozeParameters.getAlwaysOn()) { mLightRevealScrim.setVisibility(View.VISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java index b572c57590ae5..589193e383485 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java @@ -59,6 +59,7 @@ import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.charging.WiredChargingRippleController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager; @@ -202,6 +203,7 @@ public interface StatusBarPhoneModule { StatusBarTouchableRegionManager statusBarTouchableRegionManager, NotificationIconAreaController notificationIconAreaController, BrightnessSlider.Factory brightnessSliderFactory, + WiredChargingRippleController chargingRippleAnimationController, FeatureFlags featureFlags) { return new StatusBar( context, @@ -282,6 +284,7 @@ public interface StatusBarPhoneModule { statusBarTouchableRegionManager, notificationIconAreaController, brightnessSliderFactory, + chargingRippleAnimationController, featureFlags); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt new file mode 100644 index 0000000000000..3701b91271160 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt @@ -0,0 +1,115 @@ +/* + * 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.statusbar.charging + +import android.testing.AndroidTestingRunner +import android.view.View +import android.view.ViewGroupOverlay +import android.view.ViewRootImpl +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.FeatureFlags +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.statusbar.policy.KeyguardStateController +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.`when` +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class WiredChargingRippleControllerTest : SysuiTestCase() { + private lateinit var controller: WiredChargingRippleController + @Mock private lateinit var commandRegistry: CommandRegistry + @Mock private lateinit var batteryController: BatteryController + @Mock private lateinit var featureFlags: FeatureFlags + @Mock private lateinit var configurationController: ConfigurationController + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var rippleView: ChargingRippleView + @Mock private lateinit var viewHost: View + @Mock private lateinit var viewHostRootImpl: ViewRootImpl + @Mock private lateinit var viewGroupOverlay: ViewGroupOverlay + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + `when`(viewHost.viewRootImpl).thenReturn(viewHostRootImpl) + `when`(viewHostRootImpl.view).thenReturn(viewHost) + `when`(viewHost.overlay).thenReturn(viewGroupOverlay) + `when`(featureFlags.isChargingRippleEnabled).thenReturn(true) + `when`(keyguardStateController.isShowing).thenReturn(true) + controller = WiredChargingRippleController( + commandRegistry, batteryController, configurationController, + featureFlags, context, keyguardStateController) + controller.rippleView = rippleView // Replace the real ripple view with a mock instance + controller.setViewHost(viewHost) + } + + @Test + fun testSetRippleViewAsOverlay() { + val listenerCaptor = ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java) + verify(viewHost).addOnAttachStateChangeListener(listenerCaptor.capture()) + + // Fake attach to window + listenerCaptor.value.onViewAttachedToWindow(viewHost) + verify(viewGroupOverlay).add(rippleView) + } + + @Test + fun testTriggerRipple() { + val captor = ArgumentCaptor + .forClass(BatteryController.BatteryStateChangeCallback::class.java) + verify(batteryController).addCallback(captor.capture()) + + val unusedBatteryLevel = 0 + captor.value.onBatteryLevelChanged( + unusedBatteryLevel, + false /* plugged in */, + false /* charging */) + verify(rippleView, never()).startRipple() + + captor.value.onBatteryLevelChanged( + unusedBatteryLevel, + true /* plugged in */, + false /* charging */) + 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()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java index 253460db0d075..8f01b24eae1d8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java @@ -112,6 +112,7 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SuperStatusBarViewFactory; import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.charging.WiredChargingRippleController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; @@ -259,6 +260,7 @@ public class StatusBarTest extends SysuiTestCase { @Mock private DemoModeController mDemoModeController; @Mock private Lazy mNotificationShadeDepthControllerLazy; @Mock private BrightnessSlider.Factory mBrightnessSliderFactory; + @Mock private WiredChargingRippleController mWiredChargingRippleController; @Mock private FeatureFlags mFeatureFlags; private ShadeController mShadeController; private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); @@ -421,6 +423,7 @@ public class StatusBarTest extends SysuiTestCase { mStatusBarTouchableRegionManager, mNotificationIconAreaController, mBrightnessSliderFactory, + mWiredChargingRippleController, mFeatureFlags); when(mNotificationShadeWindowView.findViewById(R.id.lock_icon_container)).thenReturn(