Loading packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt +30 −5 Original line number Diff line number Diff line Loading @@ -34,8 +34,14 @@ import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.leak.RotationUtils import com.android.systemui.R import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min import kotlin.math.pow private const val MAX_DEBOUNCE_LEVEL = 3 private const val BASE_DEBOUNCE_TIME = 2000 /*** * Controls the ripple effect that shows when wired charging begins. Loading @@ -47,7 +53,9 @@ class WiredChargingRippleController @Inject constructor( batteryController: BatteryController, configurationController: ConfigurationController, featureFlags: FeatureFlags, private val context: Context private val context: Context, private val windowManager: WindowManager, private val systemClock: SystemClock ) { private var charging: Boolean? = null private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled && Loading @@ -68,6 +76,8 @@ class WiredChargingRippleController @Inject constructor( or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) setTrustedOverlay() } private var lastTriggerTime: Long? = null private var debounceLevel = 0 @VisibleForTesting var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null) Loading @@ -88,7 +98,7 @@ class WiredChargingRippleController @Inject constructor( charging = nowCharging // Only triggers when the keyguard is active and the device is just plugged in. if ((wasCharging == null || !wasCharging) && nowCharging) { startRipple() startRippleWithDebounce() } } } Loading Loading @@ -118,6 +128,22 @@ class WiredChargingRippleController @Inject constructor( updateRippleColor() } // Lazily debounce ripple to avoid triggering ripple constantly (e.g. from flaky chargers). internal fun startRippleWithDebounce() { val now = systemClock.elapsedRealtime() // Debounce wait time = 2 ^ debounce level if (lastTriggerTime == null || (now - lastTriggerTime!!) > BASE_DEBOUNCE_TIME * (2.0.pow(debounceLevel))) { // Not waiting for debounce. Start ripple. startRipple() debounceLevel = 0 } else { // Still waiting for debounce. Ignore ripple and bump debounce level. debounceLevel = min(MAX_DEBOUNCE_LEVEL, debounceLevel + 1) } lastTriggerTime = now } fun startRipple() { if (!rippleEnabled || rippleView.rippleInProgress || rippleView.parent != null) { // Skip if ripple is still playing, or not playing but already added the parent Loading @@ -125,7 +151,6 @@ class WiredChargingRippleController @Inject constructor( // the animation ends.) return } val mWM = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowLayoutParams.packageName = context.opPackageName rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewDetachedFromWindow(view: View?) {} Loading @@ -133,12 +158,12 @@ class WiredChargingRippleController @Inject constructor( override fun onViewAttachedToWindow(view: View?) { layoutRipple() rippleView.startRipple(Runnable { mWM.removeView(rippleView) windowManager.removeView(rippleView) }) rippleView.removeOnAttachStateChangeListener(this) } }) mWM.addView(rippleView, windowLayoutParams) windowManager.addView(rippleView, windowLayoutParams) } private fun layoutRipple() { Loading packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt +37 −4 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.charging import android.content.Context import android.testing.AndroidTestingRunner import android.view.View import android.view.WindowManager Loading @@ -26,7 +25,7 @@ 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.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith Loading @@ -37,6 +36,7 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.reset import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations Loading @@ -50,6 +50,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var rippleView: ChargingRippleView @Mock private lateinit var windowManager: WindowManager private val systemClock = FakeSystemClock() @Before fun setUp() { Loading @@ -57,9 +58,8 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { `when`(featureFlags.isChargingRippleEnabled).thenReturn(true) controller = WiredChargingRippleController( commandRegistry, batteryController, configurationController, featureFlags, context) featureFlags, context, windowManager, systemClock) controller.rippleView = rippleView // Replace the real ripple view with a mock instance context.addMockSystemService(Context.WINDOW_SERVICE, windowManager) } @Test Loading Loading @@ -103,4 +103,37 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { captor.value.onUiModeChanged() verify(rippleView).setColor(ArgumentMatchers.anyInt()) } @Test fun testDebounceRipple() { var time: Long = 0 systemClock.setElapsedRealtime(time) controller.startRippleWithDebounce() verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) reset(rippleView) // Wait a short while and trigger. time += 100 systemClock.setElapsedRealtime(time) controller.startRippleWithDebounce() // Verify the ripple is debounced. verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any()) // Trigger many times. for (i in 0..100) { time += 100 systemClock.setElapsedRealtime(time) controller.startRippleWithDebounce() } // Verify all attempts are debounced. verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any()) // Wait a long while and trigger. systemClock.setElapsedRealtime(time + 500000) controller.startRippleWithDebounce() // Verify that ripple is triggered. verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) } } packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java +4 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,10 @@ public class FakeSystemClock implements SystemClock { mCurrentTimeMillis = millis; } public void setElapsedRealtime(long millis) { mElapsedRealtime = millis; } /** * Advances the time tracked by the fake clock and notifies any listeners that the time has * changed (for example, an attached {@link FakeExecutor} may fire its pending runnables). Loading Loading
packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt +30 −5 Original line number Diff line number Diff line Loading @@ -34,8 +34,14 @@ import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.leak.RotationUtils import com.android.systemui.R import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject import kotlin.math.min import kotlin.math.pow private const val MAX_DEBOUNCE_LEVEL = 3 private const val BASE_DEBOUNCE_TIME = 2000 /*** * Controls the ripple effect that shows when wired charging begins. Loading @@ -47,7 +53,9 @@ class WiredChargingRippleController @Inject constructor( batteryController: BatteryController, configurationController: ConfigurationController, featureFlags: FeatureFlags, private val context: Context private val context: Context, private val windowManager: WindowManager, private val systemClock: SystemClock ) { private var charging: Boolean? = null private val rippleEnabled: Boolean = featureFlags.isChargingRippleEnabled && Loading @@ -68,6 +76,8 @@ class WiredChargingRippleController @Inject constructor( or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE) setTrustedOverlay() } private var lastTriggerTime: Long? = null private var debounceLevel = 0 @VisibleForTesting var rippleView: ChargingRippleView = ChargingRippleView(context, attrs = null) Loading @@ -88,7 +98,7 @@ class WiredChargingRippleController @Inject constructor( charging = nowCharging // Only triggers when the keyguard is active and the device is just plugged in. if ((wasCharging == null || !wasCharging) && nowCharging) { startRipple() startRippleWithDebounce() } } } Loading Loading @@ -118,6 +128,22 @@ class WiredChargingRippleController @Inject constructor( updateRippleColor() } // Lazily debounce ripple to avoid triggering ripple constantly (e.g. from flaky chargers). internal fun startRippleWithDebounce() { val now = systemClock.elapsedRealtime() // Debounce wait time = 2 ^ debounce level if (lastTriggerTime == null || (now - lastTriggerTime!!) > BASE_DEBOUNCE_TIME * (2.0.pow(debounceLevel))) { // Not waiting for debounce. Start ripple. startRipple() debounceLevel = 0 } else { // Still waiting for debounce. Ignore ripple and bump debounce level. debounceLevel = min(MAX_DEBOUNCE_LEVEL, debounceLevel + 1) } lastTriggerTime = now } fun startRipple() { if (!rippleEnabled || rippleView.rippleInProgress || rippleView.parent != null) { // Skip if ripple is still playing, or not playing but already added the parent Loading @@ -125,7 +151,6 @@ class WiredChargingRippleController @Inject constructor( // the animation ends.) return } val mWM = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager windowLayoutParams.packageName = context.opPackageName rippleView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { override fun onViewDetachedFromWindow(view: View?) {} Loading @@ -133,12 +158,12 @@ class WiredChargingRippleController @Inject constructor( override fun onViewAttachedToWindow(view: View?) { layoutRipple() rippleView.startRipple(Runnable { mWM.removeView(rippleView) windowManager.removeView(rippleView) }) rippleView.removeOnAttachStateChangeListener(this) } }) mWM.addView(rippleView, windowLayoutParams) windowManager.addView(rippleView, windowLayoutParams) } private fun layoutRipple() { Loading
packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt +37 −4 Original line number Diff line number Diff line Loading @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.charging import android.content.Context import android.testing.AndroidTestingRunner import android.view.View import android.view.WindowManager Loading @@ -26,7 +25,7 @@ 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.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith Loading @@ -37,6 +36,7 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.any import org.mockito.Mockito.eq import org.mockito.Mockito.reset import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations Loading @@ -50,6 +50,7 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var rippleView: ChargingRippleView @Mock private lateinit var windowManager: WindowManager private val systemClock = FakeSystemClock() @Before fun setUp() { Loading @@ -57,9 +58,8 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { `when`(featureFlags.isChargingRippleEnabled).thenReturn(true) controller = WiredChargingRippleController( commandRegistry, batteryController, configurationController, featureFlags, context) featureFlags, context, windowManager, systemClock) controller.rippleView = rippleView // Replace the real ripple view with a mock instance context.addMockSystemService(Context.WINDOW_SERVICE, windowManager) } @Test Loading Loading @@ -103,4 +103,37 @@ class WiredChargingRippleControllerTest : SysuiTestCase() { captor.value.onUiModeChanged() verify(rippleView).setColor(ArgumentMatchers.anyInt()) } @Test fun testDebounceRipple() { var time: Long = 0 systemClock.setElapsedRealtime(time) controller.startRippleWithDebounce() verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) reset(rippleView) // Wait a short while and trigger. time += 100 systemClock.setElapsedRealtime(time) controller.startRippleWithDebounce() // Verify the ripple is debounced. verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any()) // Trigger many times. for (i in 0..100) { time += 100 systemClock.setElapsedRealtime(time) controller.startRippleWithDebounce() } // Verify all attempts are debounced. verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any()) // Wait a long while and trigger. systemClock.setElapsedRealtime(time + 500000) controller.startRippleWithDebounce() // Verify that ripple is triggered. verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any()) } }
packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java +4 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,10 @@ public class FakeSystemClock implements SystemClock { mCurrentTimeMillis = millis; } public void setElapsedRealtime(long millis) { mElapsedRealtime = millis; } /** * Advances the time tracked by the fake clock and notifies any listeners that the time has * changed (for example, an attached {@link FakeExecutor} may fire its pending runnables). Loading