Merge "Add the charging ripple effect, currently guarded by flag_charging_ripple." into sc-dev
diff --git a/packages/SystemUI/res/values/flags.xml b/packages/SystemUI/res/values/flags.xml
index 2163806..52a97b1 100644
--- a/packages/SystemUI/res/values/flags.xml
+++ b/packages/SystemUI/res/values/flags.xml
@@ -50,4 +50,6 @@
<bool name="flag_pm_lite">false</bool>
<bool name="flag_alarm_tile">false</bool>
+
+ <bool name="flag_charging_ripple">false</bool>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
index c3de81c..708bdfe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/FeatureFlags.java
@@ -98,4 +98,8 @@
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 0000000..6f80317
--- /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 0000000..5547c1e
--- /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 0000000..b567ad4
--- /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<String>) {
+ 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 6dd00a5..427df5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -202,6 +202,8 @@
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;
@@ -377,6 +379,8 @@
private boolean mWakeUpComingFromTouch;
private PointF mWakeUpTouchLocation;
private LightRevealScrim mLightRevealScrim;
+ private ChargingRippleView mChargingRipple;
+ private WiredChargingRippleController mChargingRippleAnimationController;
private PowerButtonReveal mPowerButtonReveal;
private final Object mQueueLock = new Object();
@@ -777,6 +781,7 @@
StatusBarTouchableRegionManager statusBarTouchableRegionManager,
NotificationIconAreaController notificationIconAreaController,
BrightnessSlider.Factory brightnessSliderFactory,
+ WiredChargingRippleController chargingRippleAnimationController,
FeatureFlags featureFlags) {
super(context);
mNotificationsController = notificationsController;
@@ -855,6 +860,7 @@
mDemoModeController = demoModeController;
mNotificationIconAreaController = notificationIconAreaController;
mBrightnessSliderFactory = brightnessSliderFactory;
+ mChargingRippleAnimationController = chargingRippleAnimationController;
mFeatureFlags = featureFlags;
mExpansionChangedListeners = new ArrayList<>();
@@ -1198,6 +1204,7 @@
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 17bb449..4f32712 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
@@ -60,6 +60,7 @@
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;
@@ -204,6 +205,7 @@
StatusBarTouchableRegionManager statusBarTouchableRegionManager,
NotificationIconAreaController notificationIconAreaController,
BrightnessSlider.Factory brightnessSliderFactory,
+ WiredChargingRippleController chargingRippleAnimationController,
FeatureFlags featureFlags) {
return new StatusBar(
context,
@@ -285,6 +287,7 @@
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 0000000..3701b91
--- /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 0b454bf..781cde6c 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
@@ -113,6 +113,7 @@
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;
@@ -261,6 +262,7 @@
@Mock private DemoModeController mDemoModeController;
@Mock private Lazy<NotificationShadeDepthController> mNotificationShadeDepthControllerLazy;
@Mock private BrightnessSlider.Factory mBrightnessSliderFactory;
+ @Mock private WiredChargingRippleController mWiredChargingRippleController;
@Mock private FeatureFlags mFeatureFlags;
private ShadeController mShadeController;
private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
@@ -424,6 +426,7 @@
mStatusBarTouchableRegionManager,
mNotificationIconAreaController,
mBrightnessSliderFactory,
+ mWiredChargingRippleController,
mFeatureFlags);
when(mNotificationShadeWindowView.findViewById(R.id.lock_icon_container)).thenReturn(