Loading services/core/java/com/android/server/input/BatteryController.java +86 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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(); Loading Loading @@ -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)); } Loading @@ -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); Loading @@ -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. Loading @@ -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. Loading Loading @@ -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*/); } Loading services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +50 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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)) } } Loading
services/core/java/com/android/server/input/BatteryController.java +86 −4 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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(); Loading Loading @@ -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)); } Loading @@ -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); Loading @@ -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. Loading @@ -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. Loading Loading @@ -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*/); } Loading
services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +50 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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)) } }