diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index dcd47d4acba9b..06ee7c4d6c286 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -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,8 +273,10 @@ final class BatteryController { if (!hasRegisteredListenerForDeviceLocked(deviceId)) { // There are no more listeners monitoring this device. final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId); - monitor.onMonitorDestroy(); - mDeviceMonitors.remove(deviceId); + if (!monitor.isPersistent()) { + monitor.onMonitorDestroy(); + mDeviceMonitors.remove(deviceId); + } } if (listenerRecord.mMonitoredDevices.isEmpty()) { @@ -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 boolean anyOf(ArrayMap arrayMap, Predicate test) { + for (int i = 0; i < arrayMap.size(); i++) { + if (test.test(arrayMap.valueAt(i))) { + return true; + } + } + return false; + } } 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 65076a372b0b9..b095a506af211 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -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 = + 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, + 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,10 +208,30 @@ 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) } - devicesChangedListener.onInputDevicesChanged(list.toIntArray()) + 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 @@ -191,13 +239,6 @@ class BatteryControllerTests { 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) + } }