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

Commit 6d5a29bc authored by Prabir Pradhan's avatar Prabir Pradhan
Browse files

Introduce monitoring behavior changes for USI devices (3/n)

Extend USI battery timeout when stylus presence is detected.

The kernel only sends a UEvent for a battery node if there is a change
in the battery state. For example, when a stylus is first being used,
its battery value will be reported via a UEvent. If the stylus is used
again the next day and its battery level has not changed, no UEvent will
be produced.

To address this case, we will validate the USI battery state whenever
the presence of a stylus is detected and extend the validity timeout.

The only exception is when the stylus battery capacity is 0. When
Android first boots, the USI battery capacity will be repoted as 0. In
the event that stylus presence is detected before the battery level is
updated, we do not want to notify listeners of the capacity being 0.

Bug: 243005009
Test: atest BatteryControllerTests
Change-Id: I746496b618d7a6f51f34db65e69e7eb88c92fc87
parent 54559260
Loading
Loading
Loading
Loading
+29 −0
Original line number Diff line number Diff line
@@ -371,6 +371,17 @@ final class BatteryController {
        }
    }

    public void notifyStylusGestureStarted(int deviceId, long eventTime) {
        synchronized (mLock) {
            final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
            if (monitor == null) {
                return;
            }

            monitor.onStylusGestureStarted(eventTime);
        }
    }

    public void dump(PrintWriter pw, String prefix) {
        synchronized (mLock) {
            final String indent = prefix + "  ";
@@ -557,6 +568,8 @@ final class BatteryController {

        public void onTimeout(long eventTime) {}

        public void onStylusGestureStarted(long eventTime) {}

        // Returns the current battery state that can be used to notify listeners BatteryController.
        public State getBatteryStateForReporting() {
            return new State(mState);
@@ -599,6 +612,22 @@ final class BatteryController {
            });
        }

        @Override
        public void onStylusGestureStarted(long eventTime) {
            processChangesAndNotify(eventTime, (time) -> {
                final boolean wasValid = mValidityTimeoutCallback != null;
                if (!wasValid && mState.capacity == 0.f) {
                    // Handle a special case where the USI device reports a battery capacity of 0
                    // at boot until the first battery update. To avoid wrongly sending out a
                    // battery capacity of 0 if we detect stylus presence before the capacity
                    // is first updated, do not validate the battery state when the state is not
                    // valid and the capacity is 0.
                    return;
                }
                markUsiBatteryValid();
            });
        }

        @Override
        public void onTimeout(long eventTime) {
            processChangesAndNotify(eventTime, (time) -> markUsiBatteryInvalid());
+1 −0
Original line number Diff line number Diff line
@@ -3055,6 +3055,7 @@ public class InputManagerService extends IInputManager.Stub
    // Native callback.
    @SuppressWarnings("unused")
    private void notifyStylusGestureStarted(int deviceId, long eventTime) {
        mBatteryController.notifyStylusGestureStarted(deviceId, eventTime);
    }

    /**
+101 −1
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import androidx.test.InstrumentationRegistry
import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
import com.android.server.input.BatteryController.UEventManager
import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
import com.android.server.input.BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS
import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.MatcherAssert.assertThat
@@ -528,10 +529,109 @@ class BatteryControllerTests {
            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.64f))

        // The battery is no longer present after the timeout expires.
        testLooper.moveTimeForward(BatteryController.USI_BATTERY_VALIDITY_DURATION_MILLIS - 1)
        testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 1)
        testLooper.dispatchNext()
        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID), times(2))
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            isInvalidBatteryState(USI_DEVICE_ID))
    }

    @Test
    fun testStylusPresenceExtendsValidUsiBatteryState() {
        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING)
        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78)

        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
        testLooper.dispatchNext()
        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
        verify(uEventManager)
            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))

        // There is a UEvent signaling a battery change. The battery state is now valid.
        uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
        val listener = createMockListener()
        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))

        // Stylus presence is detected before the validity timeout expires.
        testLooper.moveTimeForward(100)
        testLooper.dispatchAll()
        batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP)

        // Ensure that timeout was extended, and the battery state is now valid for longer.
        testLooper.moveTimeForward(USI_BATTERY_VALIDITY_DURATION_MILLIS - 100)
        testLooper.dispatchAll()
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))

        // Ensure the validity period expires after the expected amount of time.
        testLooper.moveTimeForward(100)
        testLooper.dispatchNext()
        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            isInvalidBatteryState(USI_DEVICE_ID))
    }

    @Test
    fun testStylusPresenceMakesUsiBatteryStateValid() {
        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_DISCHARGING)
        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(78)

        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
        testLooper.dispatchNext()
        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
        verify(uEventManager)
            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))

        // The USI battery state is initially invalid.
        val listener = createMockListener()
        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            isInvalidBatteryState(USI_DEVICE_ID))

        // A stylus presence is detected. This validates the battery state.
        batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP)

        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f)
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            matchesState(USI_DEVICE_ID, status = STATUS_DISCHARGING, capacity = 0.78f))
    }

    @Test
    fun testStylusPresenceDoesNotMakeUsiBatteryStateValidAtBoot() {
        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
        // At boot, the USI device always reports a capacity value of 0.
        `when`(native.getBatteryStatus(USI_DEVICE_ID)).thenReturn(STATUS_UNKNOWN)
        `when`(native.getBatteryCapacity(USI_DEVICE_ID)).thenReturn(0)

        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
        testLooper.dispatchNext()
        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
        verify(uEventManager)
            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))

        // The USI battery state is initially invalid.
        val listener = createMockListener()
        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
        listener.verifyNotified(isInvalidBatteryState(USI_DEVICE_ID))
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            isInvalidBatteryState(USI_DEVICE_ID))

        // Since the capacity reported is 0, stylus presence does not validate the battery state.
        batteryController.notifyStylusGestureStarted(USI_DEVICE_ID, TIMESTAMP)

        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            isInvalidBatteryState(USI_DEVICE_ID))

        // However, if a UEvent reports a battery capacity of 0, the battery state is now valid.
        uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
        listener.verifyNotified(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f)
        assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
            matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f))
    }
}