Merge "Avoid triggering ripple repeatedly by adding debounce with an exponential falloff." into sc-dev

This commit is contained in:
Shan Huang
2021-04-28 08:16:58 +00:00
committed by Android (Google) Code Review
3 changed files with 71 additions and 9 deletions

View File

@@ -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() {

View File

@@ -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())
}
}

View File

@@ -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).