Merge "Poll for battery changes when Android is interactive"

This commit is contained in:
Prabir Pradhan
2022-09-23 13:50:24 +00:00
committed by Android (Google) Code Review
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())
}
}