Introduce monitoring behavior changes for USI devices (2/n)
- USI devices are always expected to report a UEvent whenever the battery state changes. This signals that the current stylus battery level has changed. This means we should only consider the USI battery state as valid when we receive a new UEvent. - Once a stylus battery has been reported, the sysfs battery node will report the last value perpetually, until the next change, even if the stylus is no longer being used. To handle this, we set a validity period, where the USI battery is only valid for a fixed time period (one hour) after the last time the battery changed. DD: go/inputdevice-battery-notifications Bug: 243005009 Test: atest FrameworkServicesTests Test: manual with USI 2.0 device Change-Id: I1c43a04208601c3dda19aa18f1ede3303ececc97
This commit is contained in:
@@ -65,6 +65,8 @@ final class BatteryController {
|
||||
|
||||
@VisibleForTesting
|
||||
static final long POLLING_PERIOD_MILLIS = 10_000; // 10 seconds
|
||||
@VisibleForTesting
|
||||
static final long USI_BATTERY_VALIDITY_DURATION_MILLIS = 60 * 60_000; // 1 hour
|
||||
|
||||
private final Object mLock = new Object();
|
||||
private final Context mContext;
|
||||
@@ -336,6 +338,17 @@ final class BatteryController {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMonitorTimeout(int deviceId) {
|
||||
synchronized (mLock) {
|
||||
final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
|
||||
if (monitor == null) {
|
||||
return;
|
||||
}
|
||||
final long updateTime = SystemClock.uptimeMillis();
|
||||
monitor.onTimeout(updateTime);
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets the current battery state of an input device. */
|
||||
public IInputDeviceBatteryState getBatteryState(int deviceId) {
|
||||
synchronized (mLock) {
|
||||
@@ -448,7 +461,7 @@ final class BatteryController {
|
||||
|
||||
// Holds the state of an InputDevice for which battery changes are currently being monitored.
|
||||
private class DeviceMonitor {
|
||||
private final State mState;
|
||||
protected final State mState;
|
||||
// Represents whether the input device has a sysfs battery node.
|
||||
protected boolean mHasBattery = false;
|
||||
|
||||
@@ -463,7 +476,7 @@ final class BatteryController {
|
||||
configureDeviceMonitor(eventTime);
|
||||
}
|
||||
|
||||
private void processChangesAndNotify(long eventTime, Consumer<Long> changes) {
|
||||
protected void processChangesAndNotify(long eventTime, Consumer<Long> changes) {
|
||||
final State oldState = getBatteryStateForReporting();
|
||||
changes.accept(eventTime);
|
||||
final State newState = getBatteryStateForReporting();
|
||||
@@ -521,7 +534,7 @@ final class BatteryController {
|
||||
stopMonitoring();
|
||||
}
|
||||
|
||||
private void updateBatteryStateFromNative(long eventTime) {
|
||||
protected void updateBatteryStateFromNative(long eventTime) {
|
||||
mState.updateIfChanged(
|
||||
queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery));
|
||||
}
|
||||
@@ -542,6 +555,8 @@ final class BatteryController {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onTimeout(long eventTime) {}
|
||||
|
||||
// Returns the current battery state that can be used to notify listeners BatteryController.
|
||||
public State getBatteryStateForReporting() {
|
||||
return new State(mState);
|
||||
@@ -560,10 +575,71 @@ final class BatteryController {
|
||||
// Universal Stylus Initiative (USI) protocol.
|
||||
private class UsiDeviceMonitor extends DeviceMonitor {
|
||||
|
||||
// For USI devices, we only treat the battery state as valid for a fixed amount of time
|
||||
// after receiving a battery update. Once the timeout has passed, we signal to all listeners
|
||||
// that there is no longer a battery present for the device. The battery state is valid
|
||||
// as long as this callback is non-null.
|
||||
@Nullable
|
||||
private Runnable mValidityTimeoutCallback;
|
||||
|
||||
UsiDeviceMonitor(int deviceId) {
|
||||
super(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPoll(long eventTime) {
|
||||
// Disregard polling for USI devices.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUEvent(long eventTime) {
|
||||
processChangesAndNotify(eventTime, (time) -> {
|
||||
updateBatteryStateFromNative(time);
|
||||
markUsiBatteryValid();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTimeout(long eventTime) {
|
||||
processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfiguration(long eventTime) {
|
||||
super.onConfiguration(eventTime);
|
||||
|
||||
if (!mHasBattery) {
|
||||
throw new IllegalStateException(
|
||||
"UsiDeviceMonitor: USI devices are always expected to "
|
||||
+ "report a valid battery, but no battery was detected!");
|
||||
}
|
||||
}
|
||||
|
||||
private void markUsiBatteryValid() {
|
||||
if (mValidityTimeoutCallback != null) {
|
||||
mHandler.removeCallbacks(mValidityTimeoutCallback);
|
||||
} else {
|
||||
final int deviceId = mState.deviceId;
|
||||
mValidityTimeoutCallback =
|
||||
() -> BatteryController.this.handleMonitorTimeout(deviceId);
|
||||
}
|
||||
mHandler.postDelayed(mValidityTimeoutCallback, USI_BATTERY_VALIDITY_DURATION_MILLIS);
|
||||
}
|
||||
|
||||
private void markUsiBatteryInvalid() {
|
||||
if (mValidityTimeoutCallback == null) {
|
||||
return;
|
||||
}
|
||||
mHandler.removeCallbacks(mValidityTimeoutCallback);
|
||||
mValidityTimeoutCallback = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getBatteryStateForReporting() {
|
||||
return mValidityTimeoutCallback != null
|
||||
? new State(mState) : new State(mState.deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresPolling() {
|
||||
// Do not poll the battery state for USI devices.
|
||||
@@ -575,6 +651,12 @@ final class BatteryController {
|
||||
// Do not remove the battery monitor for USI devices.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString()
|
||||
+ ", UsiStateIsValid=" + (mValidityTimeoutCallback != null);
|
||||
}
|
||||
}
|
||||
|
||||
// An interface used to change the API of UEventObserver to a more test-friendly format.
|
||||
@@ -635,7 +717,7 @@ final class BatteryController {
|
||||
}
|
||||
}
|
||||
|
||||
private void reset(int deviceId) {
|
||||
public void reset(int deviceId) {
|
||||
initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
|
||||
Float.NaN /*capacity*/);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.android.server.input
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.hardware.BatteryState.STATUS_CHARGING
|
||||
import android.hardware.BatteryState.STATUS_DISCHARGING
|
||||
import android.hardware.BatteryState.STATUS_FULL
|
||||
import android.hardware.BatteryState.STATUS_UNKNOWN
|
||||
import android.hardware.input.IInputDeviceBatteryListener
|
||||
@@ -484,4 +485,53 @@ class BatteryControllerTests {
|
||||
testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
|
||||
assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExpectedFlowForUsiBattery() {
|
||||
`when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
|
||||
`when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING)
|
||||
`when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78)
|
||||
|
||||
addInputDevice(USI_DEVICE_ID, supportsUsi = true)
|
||||
testLooper.dispatchNext()
|
||||
val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
|
||||
verify(uEventManager)
|
||||
.addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))
|
||||
|
||||
// A USI device's battery state is not valid until the first UEvent notification.
|
||||
// Add a listener, and ensure it is notified that the battery state is not present.
|
||||
val listener = createMockListener()
|
||||
batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
|
||||
listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
|
||||
|
||||
// Ensure that querying for battery state also returns the same invalid result.
|
||||
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
|
||||
isInvalidBatteryState(USI_DEVICE_ID))
|
||||
|
||||
// There is a UEvent signaling a battery change. The battery state is now valid.
|
||||
uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
|
||||
listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)
|
||||
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
|
||||
matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))
|
||||
|
||||
// There is another UEvent notification. The battery state is now updated.
|
||||
`when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(64)
|
||||
uEventListener.value!!.onBatteryUEvent(TIMESTAMP + 1)
|
||||
listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f)
|
||||
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
|
||||
matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
|
||||
|
||||
// The battery state is still valid after a millisecond.
|
||||
testLooper.moveTimeForward(1)
|
||||
testLooper.dispatchAll()
|
||||
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
|
||||
matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))
|
||||
|
||||
// The battery is no longer present after the timeout expires.
|
||||
testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1)
|
||||
testLooper.dispatchNext()
|
||||
listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2))
|
||||
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
|
||||
isInvalidBatteryState(USI_DEVICE_ID))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user