Loading services/core/java/com/android/server/input/BatteryController.java +218 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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: Loading @@ -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); } /** Loading Loading @@ -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)) Loading @@ -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 Loading Loading @@ -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); Loading @@ -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); Loading @@ -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() Loading @@ -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; Loading @@ -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(); } } } services/core/java/com/android/server/input/InputManagerService.java +3 −1 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -561,6 +561,8 @@ public class InputManagerService extends IInputManager.Stub if (mWiredAccessoryCallbacks != null) { mWiredAccessoryCallbacks.systemReady(); } mBatteryController.systemRunning(); } private void reloadKeyboardLayouts() { Loading services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +98 −10 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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}. * Loading @@ -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 Loading @@ -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)) Loading @@ -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() { Loading Loading @@ -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")) } } Loading
services/core/java/com/android/server/input/BatteryController.java +218 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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: Loading @@ -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); } /** Loading Loading @@ -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)) Loading @@ -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 Loading Loading @@ -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); Loading @@ -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); Loading @@ -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() Loading @@ -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; Loading @@ -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(); } } }
services/core/java/com/android/server/input/InputManagerService.java +3 −1 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -561,6 +561,8 @@ public class InputManagerService extends IInputManager.Stub if (mWiredAccessoryCallbacks != null) { mWiredAccessoryCallbacks.systemReady(); } mBatteryController.systemRunning(); } private void reloadKeyboardLayouts() { Loading
services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +98 −10 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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}. * Loading @@ -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 Loading @@ -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)) Loading @@ -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() { Loading Loading @@ -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")) } }