Merge "Update monitored InputDevice battery state on an UEvent notification"

This commit is contained in:
Prabir Pradhan
2022-09-19 21:53:46 +00:00
committed by Android (Google) Code Review
3 changed files with 319 additions and 22 deletions

View File

@@ -18,12 +18,17 @@ package com.android.server.input;
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UEventObserver;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -31,6 +36,8 @@ import android.util.Slog;
import android.view.InputDevice;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.input.BatteryController.UEventManager.UEventListener;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -41,7 +48,7 @@ import java.util.Set;
* A thread-safe component of {@link InputManagerService} responsible for managing the battery state
* of input devices.
*/
final class BatteryController {
final class BatteryController implements InputManager.InputDeviceListener {
private static final String TAG = BatteryController.class.getSimpleName();
// To enable these logs, run:
@@ -51,15 +58,35 @@ final class BatteryController {
private final Object mLock = new Object();
private final Context mContext;
private final NativeInputManagerService mNative;
private final Handler mHandler;
private final UEventManager mUEventManager;
// Maps a pid to the registered listener record for that process. There can only be one battery
// listener per process.
@GuardedBy("mLock")
private final ArrayMap<Integer, ListenerRecord> mListenerRecords = new ArrayMap<>();
BatteryController(Context context, NativeInputManagerService nativeService) {
// Maps a deviceId that is being monitored to the battery state for the device.
// This must be kept in sync with {@link #mListenerRecords}.
@GuardedBy("mLock")
private final ArrayMap<Integer, MonitoredDeviceState> mMonitoredDeviceStates = new ArrayMap<>();
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) {
this(context, nativeService, looper, new UEventManager() {});
}
@VisibleForTesting
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper,
UEventManager uEventManager) {
mContext = context;
mNative = nativeService;
mHandler = new Handler(looper);
mUEventManager = uEventManager;
}
void systemRunning() {
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
.registerInputDeviceListener(this, mHandler);
}
/**
@@ -96,29 +123,45 @@ final class BatteryController {
+ " is already monitoring deviceId " + deviceId);
}
MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId);
if (deviceState == null) {
// This is the first listener that is monitoring this device.
deviceState = new MonitoredDeviceState(deviceId);
mMonitoredDeviceStates.put(deviceId, deviceState);
}
if (DEBUG) {
Slog.d(TAG, "Battery listener for pid " + pid
+ " is monitoring deviceId " + deviceId);
}
notifyBatteryListener(deviceId, listenerRecord);
notifyBatteryListener(listenerRecord, deviceState);
}
}
private void notifyBatteryListener(int deviceId, ListenerRecord record) {
final long eventTime = SystemClock.uptimeMillis();
private static void notifyBatteryListener(ListenerRecord listenerRecord,
MonitoredDeviceState deviceState) {
try {
record.mListener.onBatteryStateChanged(
deviceId,
hasBattery(deviceId),
mNative.getBatteryStatus(deviceId),
mNative.getBatteryCapacity(deviceId) / 100.f,
eventTime);
listenerRecord.mListener.onBatteryStateChanged(
deviceState.mDeviceId,
deviceState.mHasBattery,
deviceState.mBatteryStatus,
deviceState.mBatteryCapacity,
deviceState.mLastUpdateTime);
} catch (RemoteException e) {
Slog.e(TAG, "Failed to notify listener", e);
}
}
@GuardedBy("mLock")
private void notifyAllListenersForDeviceLocked(MonitoredDeviceState deviceState) {
mListenerRecords.forEach((pid, listenerRecord) -> {
if (listenerRecord.mMonitoredDevices.contains(deviceState.mDeviceId)) {
notifyBatteryListener(listenerRecord, deviceState);
}
});
}
private boolean hasBattery(int deviceId) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
@@ -126,6 +169,12 @@ final class BatteryController {
return device != null && device.hasBattery();
}
@GuardedBy("mLock")
private MonitoredDeviceState getDeviceStateOrThrowLocked(int deviceId) {
return Objects.requireNonNull(mMonitoredDeviceStates.get(deviceId),
"Maps are out of sync: Cannot find device state for deviceId " + deviceId);
}
/**
* Unregister the battery listener for the given input device and stop monitoring its battery
* state. If there are no other input devices that this listener is monitoring, the listener is
@@ -170,6 +219,13 @@ final class BatteryController {
+ pid);
}
if (!hasRegisteredListenerForDeviceLocked(deviceId)) {
// There are no more listeners monitoring this device.
final MonitoredDeviceState deviceState = getDeviceStateOrThrowLocked(deviceId);
deviceState.stopMonitoring();
mMonitoredDeviceStates.remove(deviceId);
}
if (listenerRecord.mMonitoredDevices.isEmpty()) {
// There are no more devices being monitored by this listener.
listenerRecord.mListener.asBinder().unlinkToDeath(listenerRecord.mDeathRecipient, 0);
@@ -178,6 +234,16 @@ final class BatteryController {
}
}
@GuardedBy("mLock")
private boolean hasRegisteredListenerForDeviceLocked(int deviceId) {
for (int i = 0; i < mListenerRecords.size(); i++) {
if (mListenerRecords.valueAt(i).mMonitoredDevices.contains(deviceId)) {
return true;
}
}
return false;
}
private void handleListeningProcessDied(int pid) {
synchronized (mLock) {
final ListenerRecord listenerRecord = mListenerRecords.get(pid);
@@ -194,6 +260,19 @@ final class BatteryController {
}
}
// Query the battery state for the device and notify all listeners if there is a change.
private void handleBatteryChangeNotification(int deviceId, long eventTime) {
synchronized (mLock) {
final MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId);
if (deviceState == null) {
return;
}
if (deviceState.updateBatteryState(eventTime)) {
notifyAllListenersForDeviceLocked(deviceState);
}
}
}
void dump(PrintWriter pw, String prefix) {
synchronized (mLock) {
pw.println(prefix + TAG + ": " + mListenerRecords.size()
@@ -211,6 +290,29 @@ final class BatteryController {
}
}
@VisibleForTesting
@Override
public void onInputDeviceAdded(int deviceId) {}
@VisibleForTesting
@Override
public void onInputDeviceRemoved(int deviceId) {}
@VisibleForTesting
@Override
public void onInputDeviceChanged(int deviceId) {
synchronized (mLock) {
final MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId);
if (deviceState == null) {
return;
}
final long eventTime = SystemClock.uptimeMillis();
if (deviceState.updateBatteryState(eventTime)) {
notifyAllListenersForDeviceLocked(deviceState);
}
}
}
// A record of a registered battery listener from one process.
private class ListenerRecord {
final int mPid;
@@ -232,4 +334,109 @@ final class BatteryController {
+ ", monitored devices=" + Arrays.toString(mMonitoredDevices.toArray());
}
}
// Holds the state of an InputDevice for which battery changes are currently being monitored.
private class MonitoredDeviceState {
private final int mDeviceId;
private long mLastUpdateTime = 0;
private boolean mHasBattery = false;
@BatteryState.BatteryStatus
private int mBatteryStatus = BatteryState.STATUS_UNKNOWN;
private float mBatteryCapacity = Float.NaN;
@Nullable
private UEventListener mUEventListener;
MonitoredDeviceState(int deviceId) {
mDeviceId = deviceId;
// Load the initial battery state and start monitoring.
final long eventTime = SystemClock.uptimeMillis();
updateBatteryState(eventTime);
}
// Returns true if the battery state changed since the last time it was updated.
boolean updateBatteryState(long eventTime) {
mLastUpdateTime = eventTime;
final boolean batteryPresenceChanged = mHasBattery != hasBattery(mDeviceId);
if (batteryPresenceChanged) {
mHasBattery = !mHasBattery;
if (mHasBattery) {
startMonitoring();
} else {
stopMonitoring();
}
}
final int oldStatus = mBatteryStatus;
final float oldCapacity = mBatteryCapacity;
if (mHasBattery) {
mBatteryStatus = mNative.getBatteryStatus(mDeviceId);
mBatteryCapacity = mNative.getBatteryCapacity(mDeviceId) / 100.f;
} else {
mBatteryStatus = BatteryState.STATUS_UNKNOWN;
mBatteryCapacity = Float.NaN;
}
return batteryPresenceChanged
|| mBatteryStatus != oldStatus
|| mBatteryCapacity != oldCapacity;
}
private void startMonitoring() {
final String batteryPath = mNative.getBatteryDevicePath(mDeviceId);
if (batteryPath == null) {
return;
}
mUEventListener = new UEventListener() {
@Override
void onUEvent(long eventTime) {
handleBatteryChangeNotification(mDeviceId, eventTime);
}
};
mUEventManager.addListener(mUEventListener, "DEVPATH=" + batteryPath);
}
// This must be called when the device is no longer being monitored.
void stopMonitoring() {
if (mUEventListener != null) {
mUEventManager.removeListener(mUEventListener);
mUEventListener = null;
}
}
}
// An interface used to change the API of UEventObserver to a more test-friendly format.
@VisibleForTesting
interface UEventManager {
@VisibleForTesting
abstract class UEventListener {
private final UEventObserver mObserver = new UEventObserver() {
@Override
public void onUEvent(UEvent event) {
final long eventTime = SystemClock.uptimeMillis();
if (DEBUG) {
Slog.d(TAG,
"UEventListener: Received UEvent: "
+ event + " eventTime: " + eventTime);
}
UEventListener.this.onUEvent(eventTime);
}
};
abstract void onUEvent(long eventTime);
}
default void addListener(UEventListener listener, String match) {
listener.mObserver.startObserving(match);
}
default void removeListener(UEventListener listener) {
listener.mObserver.stopObserving();
}
}
}

View File

@@ -421,7 +421,7 @@ public class InputManagerService extends IInputManager.Stub
mContext = injector.getContext();
mHandler = new InputManagerHandler(injector.getLooper());
mNative = injector.getNativeService(this);
mBatteryController = new BatteryController(mContext, mNative);
mBatteryController = new BatteryController(mContext, mNative, injector.getLooper());
mUseDevInputEventForAudioJack =
mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
@@ -561,6 +561,8 @@ public class InputManagerService extends IInputManager.Stub
if (mWiredAccessoryCallbacks != null) {
mWiredAccessoryCallbacks.systemReady();
}
mBatteryController.systemRunning();
}
private void reloadKeyboardLayouts() {

View File

@@ -20,16 +20,20 @@ import android.content.Context
import android.content.ContextWrapper
import android.hardware.BatteryState.STATUS_CHARGING
import android.hardware.BatteryState.STATUS_FULL
import android.hardware.BatteryState.STATUS_UNKNOWN
import android.hardware.input.IInputDeviceBatteryListener
import android.hardware.input.IInputDevicesChangedListener
import android.hardware.input.IInputManager
import android.hardware.input.InputDeviceCountryCode
import android.hardware.input.InputManager
import android.os.Binder
import android.os.IBinder
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.UEventManager
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
@@ -39,15 +43,27 @@ import org.mockito.ArgumentMatchers.notNull
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnit
private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice =
InputDevice.Builder()
.setId(deviceId)
.setName("Device $deviceId")
.setDescriptor("descriptor $deviceId")
.setExternal(true)
.setHasBattery(hasBattery)
.setGeneration(0)
.build()
/**
* Tests for {@link InputDeviceBatteryController}.
*
@@ -60,6 +76,7 @@ class BatteryControllerTests {
const val PID = 42
const val DEVICE_ID = 13
const val SECOND_DEVICE_ID = 11
const val TIMESTAMP = 123456789L
}
@get:Rule
@@ -69,13 +86,19 @@ class BatteryControllerTests {
private lateinit var native: NativeInputManagerService
@Mock
private lateinit var iInputManager: IInputManager
@Mock
private lateinit var uEventManager: UEventManager
private lateinit var batteryController: BatteryController
private lateinit var context: Context
private lateinit var testLooper: TestLooper
private lateinit var devicesChangedListener: IInputDevicesChangedListener
private val deviceGenerationMap = mutableMapOf<Int /*deviceId*/, Int /*generation*/>()
@Before
fun setup() {
context = spy(ContextWrapper(InstrumentationRegistry.getContext()))
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))
@@ -83,17 +106,18 @@ class BatteryControllerTests {
`when`(iInputManager.getInputDevice(SECOND_DEVICE_ID))
.thenReturn(createInputDevice(SECOND_DEVICE_ID))
batteryController = BatteryController(context, native)
batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
batteryController.systemRunning()
val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java)
verify(iInputManager).registerInputDevicesChangedListener(listenerCaptor.capture())
devicesChangedListener = listenerCaptor.value
}
private fun createInputDevice(deviceId: Int): InputDevice =
InputDevice.Builder()
.setId(deviceId)
.setName("Device $deviceId")
.setDescriptor("descriptor $deviceId")
.setExternal(true)
.setHasBattery(true)
.build()
private fun notifyDeviceChanged(deviceId: Int) {
deviceGenerationMap[deviceId] = deviceGenerationMap[deviceId]?.plus(1) ?: 1
val list = deviceGenerationMap.flatMap { listOf(it.key, it.value) }
devicesChangedListener.onInputDevicesChanged(list.toIntArray())
}
@After
fun tearDown() {
@@ -169,4 +193,68 @@ class BatteryControllerTests {
verify(listener).onBatteryStateChanged(eq(SECOND_DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong())
}
@Test
fun testListenersNotifiedOnUEventNotification() {
`when`(native.getBatteryDevicePath(DEVICE_ID)).thenReturn("/test/device1")
`when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING)
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78)
val listener = createMockListener()
val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java)
batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong())
// If the battery state has changed when an UEvent is sent, the listeners are notified.
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80)
uEventListener.value!!.onUEvent(TIMESTAMP)
verify(listener).onBatteryStateChanged(DEVICE_ID, true /*isPresent*/, STATUS_CHARGING,
0.80f, TIMESTAMP)
// If the battery state has not changed when an UEvent is sent, the listeners are not
// notified.
clearInvocations(listener)
uEventListener.value!!.onUEvent(TIMESTAMP + 1)
verifyNoMoreInteractions(listener)
batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID)
verify(uEventManager).removeListener(uEventListener.capture())
assertEquals("The same observer must be registered and unregistered",
uEventListener.allValues[0], uEventListener.allValues[1])
}
@Test
fun testBatteryPresenceChanged() {
`when`(native.getBatteryDevicePath(DEVICE_ID)).thenReturn("/test/device1")
`when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING)
`when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78)
val listener = createMockListener()
val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java)
batteryController.registerBatteryListener(DEVICE_ID, listener, PID)
verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong())
// 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)
testLooper.dispatchNext()
verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(false /*isPresent*/),
eq(STATUS_UNKNOWN), eq(Float.NaN), anyLong())
// 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)
testLooper.dispatchNext()
verify(listener, times(2)).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/),
eq(STATUS_CHARGING), eq(0.78f), anyLong())
// Ensure that a new UEventListener was added.
verify(uEventManager, times(2))
.addListener(uEventListener.capture(), eq("DEVPATH=/test/device1"))
}
}