Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 89be9b07 authored by Prabir Pradhan's avatar Prabir Pradhan
Browse files

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
parent 5473b217
Loading
Loading
Loading
Loading
+57 −2
Original line number Diff line number Diff line
@@ -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));
            }
+1 −0
Original line number Diff line number Diff line
@@ -3684,6 +3684,7 @@ public class InputManagerService extends IInputManager.Stub
        @Override
        public void setInteractive(boolean interactive) {
            mNative.setInteractive(interactive);
            mBatteryController.onInteractiveChanged(interactive);
        }

        @Override
+67 −6
Original line number Diff line number Diff line
@@ -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())
    }
}