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
This commit is contained in:
@@ -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 <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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user