Merge changes I336c4c18,Ie234319c

* changes:
  Introduce monitoring behavior changes for USI devices (1/n)
  BatteryController: Notify listeners from DeviceMonitor
This commit is contained in:
Prabir Pradhan
2022-10-21 15:30:11 +00:00
committed by Android (Google) Code Review
2 changed files with 285 additions and 92 deletions

View File

@@ -44,6 +44,8 @@ import java.io.PrintWriter;
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
@@ -98,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);
}
}
/**
@@ -165,19 +171,20 @@ final class BatteryController {
}
}
@GuardedBy("mLock")
private void notifyAllListenersForDeviceLocked(State state) {
if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
mListenerRecords.forEach((pid, listenerRecord) -> {
if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
notifyBatteryListener(listenerRecord, state);
}
});
private void notifyAllListenersForDevice(State state) {
synchronized (mLock) {
if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state);
mListenerRecords.forEach((pid, listenerRecord) -> {
if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) {
notifyBatteryListener(listenerRecord, state);
}
});
}
}
@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);
@@ -192,6 +199,13 @@ final class BatteryController {
mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
}
private String getInputDeviceName(int deviceId) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
.getInputDevice(deviceId);
return device != null ? device.getName() : "<none>";
}
private boolean hasBattery(int deviceId) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -199,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),
@@ -252,8 +273,10 @@ final class BatteryController {
if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
// There are no more listeners monitoring this device.
final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId);
monitor.stopMonitoring();
mDeviceMonitors.remove(deviceId);
if (!monitor.isPersistent()) {
monitor.onMonitorDestroy();
mDeviceMonitors.remove(deviceId);
}
}
if (listenerRecord.mMonitoredDevices.isEmpty()) {
@@ -298,9 +321,7 @@ final class BatteryController {
if (monitor == null) {
return;
}
if (monitor.updateBatteryState(eventTime)) {
notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
}
monitor.onUEvent(eventTime);
}
}
@@ -310,14 +331,7 @@ final class BatteryController {
return;
}
final long eventTime = SystemClock.uptimeMillis();
mDeviceMonitors.forEach((deviceId, monitor) -> {
// Re-acquire lock in the lambda to silence error-prone build warnings.
synchronized (mLock) {
if (monitor.updateBatteryState(eventTime)) {
notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
}
}
});
mDeviceMonitors.forEach((deviceId, monitor) -> monitor.onPoll(eventTime));
mHandler.postDelayed(this::handlePollEvent, POLLING_PERIOD_MILLIS);
}
}
@@ -329,15 +343,11 @@ final class BatteryController {
final DeviceMonitor monitor = mDeviceMonitors.get(deviceId);
if (monitor == null) {
// The input device's battery is not being monitored by any listener.
return queryBatteryStateFromNative(deviceId, updateTime);
return queryBatteryStateFromNative(deviceId, updateTime, hasBattery(deviceId));
}
// Force the battery state to update, and notify listeners if necessary.
final boolean stateChanged = monitor.updateBatteryState(updateTime);
final State state = monitor.getBatteryStateForReporting();
if (stateChanged) {
notifyAllListenersForDeviceLocked(state);
}
return state;
monitor.onPoll(updateTime);
return monitor.getBatteryStateForReporting();
}
}
@@ -379,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) {}
@@ -392,9 +409,7 @@ final class BatteryController {
return;
}
final long eventTime = SystemClock.uptimeMillis();
if (monitor.updateBatteryState(eventTime)) {
notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting());
}
monitor.onConfiguration(eventTime);
}
}
};
@@ -422,8 +437,7 @@ final class BatteryController {
}
// Queries the battery state of an input device from native code.
private State queryBatteryStateFromNative(int deviceId, long updateTime) {
final boolean isPresent = hasBattery(deviceId);
private State queryBatteryStateFromNative(int deviceId, long updateTime, boolean isPresent) {
return new State(
deviceId,
updateTime,
@@ -434,8 +448,9 @@ final class BatteryController {
// Holds the state of an InputDevice for which battery changes are currently being monitored.
private class DeviceMonitor {
@NonNull
private State mState;
private final State mState;
// Represents whether the input device has a sysfs battery node.
protected boolean mHasBattery = false;
@Nullable
private UEventBatteryListener mUEventBatteryListener;
@@ -445,26 +460,32 @@ final class BatteryController {
// Load the initial battery state and start monitoring.
final long eventTime = SystemClock.uptimeMillis();
updateBatteryState(eventTime);
configureDeviceMonitor(eventTime);
}
// Returns true if the battery state changed since the last time it was updated.
public boolean updateBatteryState(long updateTime) {
mState.updateTime = updateTime;
final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime);
if (mState.equals(updatedState)) {
return false;
private void processChangesAndNotify(long eventTime, Consumer<Long> changes) {
final State oldState = getBatteryStateForReporting();
changes.accept(eventTime);
final State newState = getBatteryStateForReporting();
if (!oldState.equals(newState)) {
notifyAllListenersForDevice(newState);
}
if (mState.isPresent != updatedState.isPresent) {
if (updatedState.isPresent) {
}
public void onConfiguration(long eventTime) {
processChangesAndNotify(eventTime, this::configureDeviceMonitor);
}
private void configureDeviceMonitor(long eventTime) {
if (mHasBattery != hasBattery(mState.deviceId)) {
mHasBattery = !mHasBattery;
if (mHasBattery) {
startMonitoring();
} else {
stopMonitoring();
}
updateBatteryStateFromNative(eventTime);
}
mState = updatedState;
return true;
}
private void startMonitoring() {
@@ -483,19 +504,44 @@ final class BatteryController {
mUEventBatteryListener, "DEVPATH=" + formatDevPath(batteryPath));
}
private String formatDevPath(String path) {
private String formatDevPath(@NonNull String path) {
// Remove the "/sys" prefix if it has one.
return path.startsWith("/sys") ? path.substring(4) : path;
}
// This must be called when the device is no longer being monitored.
public void stopMonitoring() {
private void stopMonitoring() {
if (mUEventBatteryListener != null) {
mUEventManager.removeListener(mUEventBatteryListener);
mUEventBatteryListener = null;
}
}
// This must be called when the device is no longer being monitored.
public void onMonitorDestroy() {
stopMonitoring();
}
private void updateBatteryStateFromNative(long eventTime) {
mState.updateIfChanged(
queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery));
}
public void onPoll(long eventTime) {
processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
}
public void onUEvent(long eventTime) {
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);
@@ -503,8 +549,31 @@ final class BatteryController {
@Override
public String toString() {
return "state=" + mState
+ ", uEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
return "DeviceId=" + mState.deviceId
+ ", Name='" + getInputDeviceName(mState.deviceId) + "'"
+ ", NativeBattery=" + mState
+ ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
}
}
// 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;
}
}
@@ -548,18 +617,33 @@ final class BatteryController {
private static class State extends IInputDeviceBatteryState {
State(int deviceId) {
initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
Float.NaN /*capacity*/);
reset(deviceId);
}
State(IInputDeviceBatteryState s) {
initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
copyFrom(s);
}
State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) {
initialize(deviceId, updateTime, isPresent, status, capacity);
}
// Updates this from other if there is a difference between them, ignoring the updateTime.
public void updateIfChanged(IInputDeviceBatteryState other) {
if (!equalsIgnoringUpdateTime(other)) {
copyFrom(other);
}
}
private void reset(int deviceId) {
initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN,
Float.NaN /*capacity*/);
}
private void copyFrom(IInputDeviceBatteryState s) {
initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity);
}
private void initialize(int deviceId, long updateTime, boolean isPresent, int status,
float capacity) {
this.deviceId = deviceId;
@@ -569,11 +653,34 @@ final class BatteryController {
this.capacity = capacity;
}
private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) {
long updateTime = this.updateTime;
this.updateTime = other.updateTime;
boolean eq = this.equals(other);
this.updateTime = updateTime;
return eq;
}
@Override
public String toString() {
return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime
+ ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity
+ " }";
if (!isPresent) {
return "State{<not present>}";
}
return "State{time=" + updateTime
+ ", isPresent=" + isPresent
+ ", status=" + status
+ ", capacity=" + capacity
+ "}";
}
}
// 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;
}
}

View File

@@ -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)
}
}