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

Commit eef47dbf authored by Prabir Pradhan's avatar Prabir Pradhan
Browse files

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

- Whether the USI battery state is valid will depend on when we last got
  a battery state update. This means that battery for USI devices must
  always be monitored so that we know whether the state is valid when
  the first listener is registered. We add the concept of a perpetual
  monitor that is added at boot and never removed.
- Since USI battery state changes are always accompanied by UEvents,
  there is no need to poll for battery changes. Make sure that we don't
  poll for battery changes if we're only listening to USI devices.

Bug: 243005009
Test: atest FrameworkServicesTests
Change-Id: I336c4c183d7f44ebdbd93a130fe204ddcad4617c
parent d2bf2b4c
Loading
Loading
Loading
Loading
+66 −6
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;

/**
 * A thread-safe component of {@link InputManagerService} responsible for managing the battery state
@@ -99,8 +100,12 @@ final class BatteryController {
    }

    public void systemRunning() {
        Objects.requireNonNull(mContext.getSystemService(InputManager.class))
                .registerInputDeviceListener(mInputDeviceListener, mHandler);
        final InputManager inputManager =
                Objects.requireNonNull(mContext.getSystemService(InputManager.class));
        inputManager.registerInputDeviceListener(mInputDeviceListener, mHandler);
        for (int deviceId : inputManager.getInputDeviceIds()) {
            mInputDeviceListener.onInputDeviceAdded(deviceId);
        }
    }

    /**
@@ -179,7 +184,7 @@ final class BatteryController {

    @GuardedBy("mLock")
    private void updatePollingLocked(boolean delayStart) {
        if (mDeviceMonitors.isEmpty() || !mIsInteractive) {
        if (!mIsInteractive || !anyOf(mDeviceMonitors, DeviceMonitor::requiresPolling)) {
            // Stop polling.
            mIsPolling = false;
            mHandler.removeCallbacks(this::handlePollEvent);
@@ -208,6 +213,13 @@ final class BatteryController {
        return device != null && device.hasBattery();
    }

    private boolean isUsiDevice(int deviceId) {
        final InputDevice device =
                Objects.requireNonNull(mContext.getSystemService(InputManager.class))
                        .getInputDevice(deviceId);
        return device != null && device.supportsUsi();
    }

    @GuardedBy("mLock")
    private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) {
        return Objects.requireNonNull(mDeviceMonitors.get(deviceId),
@@ -261,9 +273,11 @@ final class BatteryController {
        if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
            // There are no more listeners monitoring this device.
            final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId);
            if (!monitor.isPersistent()) {
                monitor.onMonitorDestroy();
                mDeviceMonitors.remove(deviceId);
            }
        }

        if (listenerRecord.mMonitoredDevices.isEmpty()) {
            // There are no more devices being monitored by this listener.
@@ -375,7 +389,14 @@ final class BatteryController {
    private final InputManager.InputDeviceListener mInputDeviceListener =
            new InputManager.InputDeviceListener() {
        @Override
        public void onInputDeviceAdded(int deviceId) {}
        public void onInputDeviceAdded(int deviceId) {
            synchronized (mLock) {
                if (isUsiDevice(deviceId) && !mDeviceMonitors.containsKey(deviceId)) {
                    // Start monitoring USI device immediately.
                    mDeviceMonitors.put(deviceId, new UsiDeviceMonitor(deviceId));
                }
            }
        }

        @Override
        public void onInputDeviceRemoved(int deviceId) {}
@@ -513,6 +534,14 @@ final class BatteryController {
            processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
        }

        public boolean requiresPolling() {
            return true;
        }

        public boolean isPersistent() {
            return false;
        }

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

    // Battery monitoring logic that is specific to stylus devices that support the
    // Universal Stylus Initiative (USI) protocol.
    private class UsiDeviceMonitor extends DeviceMonitor {

        UsiDeviceMonitor(int deviceId) {
            super(deviceId);
        }

        @Override
        public boolean requiresPolling() {
            // Do not poll the battery state for USI devices.
            return false;
        }

        @Override
        public boolean isPersistent() {
            // Do not remove the battery monitor for USI devices.
            return true;
        }
    }

    // An interface used to change the API of UEventObserver to a more test-friendly format.
    @VisibleForTesting
    interface UEventManager {
@@ -623,4 +673,14 @@ final class BatteryController {
                    + "}";
        }
    }

    // Check if any value in an ArrayMap matches the predicate in an optimized way.
    private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
        for (int i = 0; i < arrayMap.size(); i++) {
            if (test.test(arrayMap.valueAt(i))) {
                return true;
            }
        }
        return false;
    }
}
+116 −30
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
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 org.hamcrest.Description
@@ -42,6 +43,8 @@ import org.hamcrest.TypeSafeMatcher
import org.hamcrest.core.IsEqual.equalTo
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
@@ -63,14 +66,20 @@ import org.mockito.hamcrest.MockitoHamcrest
import org.mockito.junit.MockitoJUnit
import org.mockito.verification.VerificationMode

private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice =
private fun createInputDevice(
    deviceId: Int,
    hasBattery: Boolean = true,
    supportsUsi: Boolean = false,
    generation: Int = -1,
): InputDevice =
    InputDevice.Builder()
        .setId(deviceId)
        .setName("Device $deviceId")
        .setDescriptor("descriptor $deviceId")
        .setExternal(true)
        .setHasBattery(hasBattery)
        .setGeneration(0)
        .setSupportsUsi(supportsUsi)
        .setGeneration(generation)
        .build()

// Returns a matcher that helps match member variables of a class.
@@ -118,7 +127,10 @@ private fun matchesState(
    return Matchers.allOf(batteryStateMatchers)
}

// Helper used to verify interactions with a mocked battery listener.
private fun isInvalidBatteryState(deviceId: Int): Matcher<IInputDeviceBatteryState> =
    matchesState(deviceId, isPresent = false, status = STATUS_UNKNOWN, capacity = Float.NaN)

// Helpers used to verify interactions with a mocked battery listener.
private fun IInputDeviceBatteryListener.verifyNotified(
    deviceId: Int,
    mode: VerificationMode = times(1),
@@ -127,8 +139,21 @@ private fun IInputDeviceBatteryListener.verifyNotified(
    capacity: Float? = null,
    eventTime: Long? = null
) {
    verify(this, mode).onBatteryStateChanged(
        MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime)))
    verifyNotified(matchesState(deviceId, isPresent, status, capacity, eventTime), mode)
}

private fun IInputDeviceBatteryListener.verifyNotified(
    matcher: Matcher<IInputDeviceBatteryState>,
    mode: VerificationMode = times(1)
) {
    verify(this, mode).onBatteryStateChanged(MockitoHamcrest.argThat(matcher))
}

private fun createMockListener(): IInputDeviceBatteryListener {
    val listener = mock(IInputDeviceBatteryListener::class.java)
    val binder = mock(Binder::class.java)
    `when`(listener.asBinder()).thenReturn(binder)
    return listener
}

/**
@@ -143,6 +168,8 @@ class BatteryControllerTests {
        const val PID = 42
        const val DEVICE_ID = 13
        const val SECOND_DEVICE_ID = 11
        const val USI_DEVICE_ID = 101
        const val SECOND_USI_DEVICE_ID = 102
        const val TIMESTAMP = 123456789L
    }

@@ -168,10 +195,11 @@ class BatteryControllerTests {
        testLooper = TestLooper()
        val inputManager = InputManager.resetInstance(iInputManager)
        `when`(context.getSystemService(eq(Context.INPUT_SERVICE))).thenReturn(inputManager)
        `when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID, SECOND_DEVICE_ID))
        `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(createInputDevice(DEVICE_ID))
        `when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
            .thenReturn(createInputDevice(SECOND_DEVICE_ID))
        `when`(iInputManager.inputDeviceIds).then {
            deviceGenerationMap.keys.toIntArray()
        }
        addInputDevice(DEVICE_ID)
        addInputDevice(SECOND_DEVICE_ID)

        batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
        batteryController.systemRunning()
@@ -180,24 +208,37 @@ class BatteryControllerTests {
        devicesChangedListener = listenerCaptor.value
    }

    private fun notifyDeviceChanged(deviceId: Int) {
        deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1
    private fun notifyDeviceChanged(
            deviceId: Int,
        hasBattery: Boolean = true,
        supportsUsi: Boolean = false
    ) {
        val generation = deviceGenerationMap[deviceId]?.plus(1)
            ?: throw IllegalArgumentException("Device $deviceId was never added!")
        deviceGenerationMap[deviceId] = generation

        `when`(iInputManager.getInputDevice(deviceId))
            .thenReturn(createInputDevice(deviceId, hasBattery, supportsUsi, generation))
        val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) }
        if (::devicesChangedListener.isInitialized) {
            devicesChangedListener.onInputDevicesChanged(list.toIntArray())
        }
    }

    private fun addInputDevice(
            deviceId: Int,
        hasBattery: Boolean = true,
        supportsUsi: Boolean = false
    ) {
        deviceGenerationMap[deviceId] = 0
        notifyDeviceChanged(deviceId, hasBattery, supportsUsi)
    }

    @After
    fun tearDown() {
        InputManager.clearInstance()
    }

    private fun createMockListener(): IInputDeviceBatteryListener {
        val listener = mock(IInputDeviceBatteryListener::class.java)
        val binder = mock(Binder::class.java)
        `when`(listener.asBinder()).thenReturn(binder)
        return listener
    }

    @Test
    fun testRegisterAndUnregisterBinderLifecycle() {
        val listener = createMockListener()
@@ -303,19 +344,14 @@ class BatteryControllerTests {
        listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)

        // 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)
        notifyDeviceChanged(DEVICE_ID, hasBattery = false)
        testLooper.dispatchNext()
        listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN,
            capacity = Float.NaN)
        listener.verifyNotified(isInvalidBatteryState(DEVICE_ID))
        // Since the battery is no longer present, the UEventListener should be removed.
        verify(uEventManager).removeListener(uEventListener.value)

        // If the battery becomes present again, the listener is notified.
        `when`(iInputManager.getInputDevice(DEVICE_ID))
            .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true))
        notifyDeviceChanged(DEVICE_ID)
        notifyDeviceChanged(DEVICE_ID, hasBattery = true)
        testLooper.dispatchNext()
        listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING,
            capacity = 0.78f)
@@ -340,9 +376,17 @@ class BatteryControllerTests {

        // Move the time forward so that the polling period has elapsed.
        // The listener should be notified.
        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1)
        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS - 1)
        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
        testLooper.dispatchNext()
        listener.verifyNotified(DEVICE_ID, capacity = 0.80f)

        // Move the time forward so that another polling period has elapsed.
        // The battery should still be polled, but there is no change so listeners are not notified.
        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
        testLooper.dispatchNext()
        listener.verifyNotified(DEVICE_ID, mode = times(1), capacity = 0.80f)
    }

    @Test
@@ -357,7 +401,8 @@ class BatteryControllerTests {
        // 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.moveTimeForward(POLLING_PERIOD_MILLIS)
        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
        testLooper.dispatchAll()
        listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f)

@@ -368,7 +413,8 @@ class BatteryControllerTests {

        // Ensure that we continue to poll for battery changes.
        `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90)
        testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS)
        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
        assertTrue("There should be a polling callbacks posted to the handler", testLooper.isIdle)
        testLooper.dispatchNext()
        listener.verifyNotified(DEVICE_ID, capacity = 0.90f)
    }
@@ -398,4 +444,44 @@ class BatteryControllerTests {
            matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f))
        listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)
    }

    @Test
    fun testUsiDeviceIsMonitoredPersistently() {
        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
        testLooper.dispatchNext()

        // Even though there is no listener added for this device, it is being monitored.
        val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
        verify(uEventManager)
            .addListener(uEventListener.capture(), eq("DEVPATH=/dev/usi_device"))

        // Add and remove a listener for the device.
        val listener = createMockListener()
        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)
        batteryController.unregisterBatteryListener(USI_DEVICE_ID, listener, PID)

        // The device is still being monitored.
        verify(uEventManager, never()).removeListener(uEventListener.value)
    }

    @Test
    fun testNoPollingWhenUsiDevicesAreMonitored() {
        `when`(native.getBatteryDevicePath(USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device")
        addInputDevice(USI_DEVICE_ID, supportsUsi = true)
        testLooper.dispatchNext()
        `when`(native.getBatteryDevicePath(SECOND_USI_DEVICE_ID)).thenReturn("/sys/dev/usi_device2")
        addInputDevice(SECOND_USI_DEVICE_ID, supportsUsi = true)
        testLooper.dispatchNext()

        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)

        // Add a listener.
        val listener = createMockListener()
        batteryController.registerBatteryListener(USI_DEVICE_ID, listener, PID)

        testLooper.moveTimeForward(POLLING_PERIOD_MILLIS)
        assertFalse("There should be no polling callbacks posted to the handler", testLooper.isIdle)
    }
}