From 89be9b073ce17fee9783e1a91e75913f8e911c93 Mon Sep 17 00:00:00 2001 From: Prabir Pradhan Date: Thu, 25 Aug 2022 22:24:29 +0000 Subject: [PATCH] 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 --- .../server/input/BatteryController.java | 59 ++++++++++++++- .../server/input/InputManagerService.java | 1 + .../server/input/BatteryControllerTests.kt | 73 +++++++++++++++++-- 3 files changed, 125 insertions(+), 8 deletions(-) diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 86732a12ec2c0..32f17dc18033a 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -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 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)); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 4da0c0df390c4..05c4f77d5c812 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3684,6 +3684,7 @@ public class InputManagerService extends IInputManager.Stub @Override public void setInteractive(boolean interactive) { mNative.setInteractive(interactive); + mBatteryController.onInteractiveChanged(interactive); } @Override 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 d118661b84f5c..40757182890c8 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -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()) + } }