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

Commit 7f105814 authored by Shan Huang's avatar Shan Huang Committed by Android (Google) Code Review
Browse files

Merge "Avoid triggering ripple repeatedly by adding debounce with an...

Merge "Avoid triggering ripple repeatedly by adding debounce with an exponential falloff." into sc-dev
parents 9ccd991d c53f8c18
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).