Poll for battery changes when Android is interactive

Poll for battery changes for input devices when there is at least one
battery listener and the Android device is actively being used by the
user.

Polling for battery changes is a fallback to ensure that the battery
state for a monitored device is updated regularly, even if the kernel
does not notify userspace about changes to the device's battery. Since
this is only used as a fallback, we only poll for the battery state of
monitored devices once every 5 minutes.

DD: go/inputdevice-battery-notifications

Bug: 243005009
Test: atest InputDeviceBatteryControllerTests
Change-Id: I0ddd019881efeccee07d088bc0e6d6ff9b0d1a54
This commit is contained in:
Prabir Pradhan
2022-08-25 22:24:29 +00:00
parent 5473b217d7
commit 89be9b073c
3 changed files with 125 additions and 8 deletions

View File

@@ -55,6 +55,9 @@ final class BatteryController implements InputManager.InputDeviceListener {
// 'adb shell setprop log.tag.BatteryController DEBUG' (requires restart)
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@VisibleForTesting
static final long POLLING_PERIOD_MILLIS = 10_000; // 10 seconds
private final Object mLock = new Object();
private final Context mContext;
private final NativeInputManagerService mNative;
@@ -71,6 +74,11 @@ final class BatteryController implements InputManager.InputDeviceListener {
@GuardedBy("mLock")
private final ArrayMap<Integer, MonitoredDeviceState> mMonitoredDeviceStates = new ArrayMap<>();
@GuardedBy("mLock")
private boolean mIsPolling = false;
@GuardedBy("mLock")
private boolean mIsInteractive = true;
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) {
this(context, nativeService, looper, new UEventManager() {});
}
@@ -135,6 +143,7 @@ final class BatteryController implements InputManager.InputDeviceListener {
+ " is monitoring deviceId " + deviceId);
}
updatePollingLocked(true /*delayStart*/);
notifyBatteryListener(listenerRecord, deviceState);
}
}
@@ -162,6 +171,23 @@ final class BatteryController implements InputManager.InputDeviceListener {
});
}
@GuardedBy("mLock")
private void updatePollingLocked(boolean delayStart) {
if (mMonitoredDeviceStates.isEmpty() || !mIsInteractive) {
// Stop polling.
mIsPolling = false;
mHandler.removeCallbacks(this::handlePollEvent);
return;
}
if (mIsPolling) {
return;
}
// Start polling.
mIsPolling = true;
mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
}
private boolean hasBattery(int deviceId) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -232,6 +258,8 @@ final class BatteryController implements InputManager.InputDeviceListener {
mListenerRecords.remove(pid);
if (DEBUG) Slog.d(TAG, "Battery listener removed for pid " + pid);
}
updatePollingLocked(false /*delayStart*/);
}
@GuardedBy("mLock")
@@ -273,10 +301,37 @@ final class BatteryController implements InputManager.InputDeviceListener {
}
}
private void handlePollEvent() {
synchronized (mLock) {
if (!mIsPolling) {
return;
}
final long eventTime = SystemClock.uptimeMillis();
mMonitoredDeviceStates.forEach((deviceId, deviceState) -> {
// Re-acquire lock in the lambda to silence error-prone build warnings.
synchronized (mLock) {
if (deviceState.updateBatteryState(eventTime)) {
notifyAllListenersForDeviceLocked(deviceState);
}
}
});
mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS);
}
}
void onInteractiveChanged(boolean interactive) {
synchronized (mLock) {
mIsInteractive = interactive;
updatePollingLocked(false /*delayStart*/);
}
}
void dump(PrintWriter pw, String prefix) {
synchronized (mLock) {
pw.println(prefix + TAG + ": " + mListenerRecords.size()
+ " battery listeners");
pw.println(prefix + TAG + ": "
+ mListenerRecords.size() + " battery listeners"
+ ", Polling = " + mIsPolling
+ ", Interactive = " + mIsInteractive);
for (int i = 0; i < mListenerRecords.size(); i++) {
pw.println(prefix + " " + i + ": " + mListenerRecords.valueAt(i));
}

View File

@@ -3684,6 +3684,7 @@ public class InputManagerService extends IInputManager.Stub
@Override
public void setInteractive(boolean interactive) {
mNative.setInteractive(interactive);
mBatteryController.onInteractiveChanged(interactive);
}
@Override

View File

@@ -233,16 +233,20 @@ class BatteryControllerTests {
val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java)
batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong())
verify(listener).onBatteryStateChanged(
eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong()
)
// If the battery presence for the InputDevice changes, the listener is notified.
`when`(iInputManager.getInputDevice(DEVICE_ID))
.thenReturn(createInputDevice(DEVICE_ID, hasBattery = false))
notifyDeviceChanged(DEVICE_ID)
testLooper.dispatchNext()
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(false /*isPresent*/),
eq(STATUS_UNKNOWN), eq(Float.NaN), anyLong())
verify(listener).onBatteryStateChanged(
eq(DEVICE_ID), eq(false /*isPresent*/),
eq(STATUS_UNKNOWN), eq(Float.NaN), anyLong()
)
// Since the battery is no longer present, the UEventListener should be removed.
verify(uEventManager).removeListener(uEventListener.value)
@@ -251,10 +255,67 @@ class BatteryControllerTests {
.thenReturn(createInputDevice(DEVICE_ID, hasBattery = true))
notifyDeviceChanged(DEVICE_ID)
testLooper.dispatchNext()
verify(listener, times(2)).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong())
verify(listener, times(2)).onBatteryStateChanged(
eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong()
)
// Ensure that a new UEventListener was added.
verify(uEventManager, times(2))
.addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
}
fun testStartPollingWhenListenerIsRegistered() {
val listener = createMockListener()
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78)
batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), anyInt(),
eq(0.78f), anyLong())
// Assume there is a change in the battery state. Ensure the listener is not notified
// while the polling period has not elapsed.
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
testLooper.moveTimeForward(1)
testLooper.dispatchAll()
verify(listener, never()).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
anyInt(), eq(0.80f), anyLong())
// Move the time forward so that the polling period has elapsed.
// The listener should be notified.
testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1)
testLooper.dispatchNext()
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), anyInt(),
eq(0.80f), anyLong())
}
@Test
fun testNoPollingWhenTheDeviceIsNotInteractive() {
batteryController.onInteractiveChanged(false /*interactive*/)
val listener = createMockListener()
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78)
batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), anyInt(),
eq(0.78f), anyLong())
// The battery state changed, but we should not be polling for battery changes when the
// device is not interactive.
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
testLooper.dispatchAll()
verify(listener, never()).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
anyInt(), eq(0.80f), anyLong())
// The device is now interactive. Battery state polling begins immediately.
batteryController.onInteractiveChanged(true /*interactive*/)
testLooper.dispatchNext()
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
anyInt(), eq(0.80f), anyLong())
// Ensure that we continue to poll for battery changes.
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90)
testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
testLooper.dispatchNext()
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
anyInt(), eq(0.90f), anyLong())
}
}