Merge "Avoid triggering ripple repeatedly by adding debounce with an exponential falloff." into sc-dev
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user