diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 06ee7c4d6c286..36199debaa6ec 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -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 changes) { + protected void processChangesAndNotify(long eventTime, Consumer 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*/); } diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index b095a506af211..c68db3460dac3 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -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)) + } }