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

Commit 4b90718a authored by Prabir Pradhan's avatar Prabir Pradhan Committed by Android (Google) Code Review
Browse files

Merge "Poll for battery changes when Android is interactive"

parents 9be01f68 89be9b07
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())
    }
}