From c53f8c18515904c2961ef0ebc7d98c62ed60ce87 Mon Sep 17 00:00:00 2001 From: Shan Huang Date: Tue, 27 Apr 2021 23:32:17 +0800 Subject: [PATCH] Avoid triggering ripple repeatedly by adding debounce with an exponential falloff. Bug: 186426146 Bug: 184912594 Test: Manual. WiredChargingRippleControllerTest. Change-Id: I132e7557f805d338e7051ccdb96223c15d2711a1 --- .../charging/WiredChargingRippleController.kt | 35 +++++++++++++--- .../WiredChargingRippleControllerTest.kt | 41 +++++++++++++++++-- .../systemui/util/time/FakeSystemClock.java | 4 ++ 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt index 3bf1ff2cf7fa9..761a1d6801bf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/charging/WiredChargingRippleController.kt @@ -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. @@ -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 && @@ -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) @@ -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() } } } @@ -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 @@ -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?) {} @@ -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() { 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 index 5e783a53a1e1a..4e404ae94bd94 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/charging/WiredChargingRippleControllerTest.kt @@ -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 @@ -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 @@ -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 @@ -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() { @@ -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 @@ -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()) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java index d8271e8292be7..db6164dcf8f98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java @@ -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).