Merge "Add the charging ripple effect, currently guarded by flag_charging_ripple." into sc-dev
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -98,4 +98,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +202,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;
|
||||
@@ -377,6 +379,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();
|
||||
@@ -777,6 +781,7 @@ public class StatusBar extends SystemUI implements DemoMode,
|
||||
StatusBarTouchableRegionManager statusBarTouchableRegionManager,
|
||||
NotificationIconAreaController notificationIconAreaController,
|
||||
BrightnessSlider.Factory brightnessSliderFactory,
|
||||
WiredChargingRippleController chargingRippleAnimationController,
|
||||
FeatureFlags featureFlags) {
|
||||
super(context);
|
||||
mNotificationsController = notificationsController;
|
||||
@@ -855,6 +860,7 @@ public class StatusBar extends SystemUI implements DemoMode,
|
||||
mDemoModeController = demoModeController;
|
||||
mNotificationIconAreaController = notificationIconAreaController;
|
||||
mBrightnessSliderFactory = brightnessSliderFactory;
|
||||
mChargingRippleAnimationController = chargingRippleAnimationController;
|
||||
mFeatureFlags = featureFlags;
|
||||
|
||||
mExpansionChangedListeners = new ArrayList<>();
|
||||
@@ -1198,6 +1204,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);
|
||||
|
||||
@@ -60,6 +60,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;
|
||||
@@ -204,6 +205,7 @@ public interface StatusBarPhoneModule {
|
||||
StatusBarTouchableRegionManager statusBarTouchableRegionManager,
|
||||
NotificationIconAreaController notificationIconAreaController,
|
||||
BrightnessSlider.Factory brightnessSliderFactory,
|
||||
WiredChargingRippleController chargingRippleAnimationController,
|
||||
FeatureFlags featureFlags) {
|
||||
return new StatusBar(
|
||||
context,
|
||||
@@ -285,6 +287,7 @@ public interface StatusBarPhoneModule {
|
||||
statusBarTouchableRegionManager,
|
||||
notificationIconAreaController,
|
||||
brightnessSliderFactory,
|
||||
chargingRippleAnimationController,
|
||||
featureFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -113,6 +113,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;
|
||||
@@ -261,6 +262,7 @@ public class StatusBarTest extends SysuiTestCase {
|
||||
@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 @@ public class StatusBarTest extends SysuiTestCase {
|
||||
mStatusBarTouchableRegionManager,
|
||||
mNotificationIconAreaController,
|
||||
mBrightnessSliderFactory,
|
||||
mWiredChargingRippleController,
|
||||
mFeatureFlags);
|
||||
|
||||
when(mNotificationShadeWindowView.findViewById(R.id.lock_icon_container)).thenReturn(
|
||||
|
||||
Reference in New Issue
Block a user