diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index c57d7e79c12a7..86732a12ec2c0 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -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 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 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(); + } + } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 3def714f0c304..4da0c0df390c4 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -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() { diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index 6e422fa7af7f1..d118661b84f5c 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -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() @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")) + } }