diff options
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 86732a12ec2c..32f17dc18033 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<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)); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 4da0c0df390c..05c4f77d5c81 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 d118661b84f5..40757182890c 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()) + } } |