Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c53f8c18 authored by Shan Huang's avatar Shan Huang
Browse files

Avoid triggering ripple repeatedly by adding debounce with an exponential falloff.

Bug: 186426146
Bug: 184912594
Test: Manual. WiredChargingRippleControllerTest.
Change-Id: I132e7557f805d338e7051ccdb96223c15d2711a1
parent 1b2c6656
Loading
Loading
Loading
Loading
+30 −5
Original line number Diff line number Diff line
@@ -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() {
+37 −4
Original line number Diff line number Diff line
@@ -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())
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -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).