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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -3684,6 +3684,7 @@ public class InputManagerService extends IInputManager.Stub
|
||||
@Override
|
||||
public void setInteractive(boolean interactive) {
|
||||
mNative.setInteractive(interactive);
|
||||
mBatteryController.onInteractiveChanged(interactive);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user