From b6f775cea23fd29b16f337c6be2ca70a4371ead7 Mon Sep 17 00:00:00 2001 From: Prabir Pradhan Date: Thu, 18 Aug 2022 14:47:23 +0000 Subject: [PATCH] Add InputDeviceBatteryListener to InputManager System apps can now register an InputDeviceBatteryListener on a per-device basis to be notified of battery changes. - InputManager manages registration of the binder callback with InputManagerService to ensure that there is only one battery listener per process. - The battery listener is invoked immediately upon registration to provide the initial battery state for the device. - The callbacks are removed automatically when the process dies. - This CL does not yet identify changes in battery state for monitored devices. DD: go/inputdevice-battery-notifications Bug: 243005009 Test: atest FrameworkServicesTests Test: atest FrameworkCoreTests Change-Id: Idf2b06a099155a6f5cc13b55dcd40852f54aa56c Merged-In: Idf2b06a099155a6f5cc13b55dcd40852f54aa56c (cherry picked from commit e86f73ac50a26d5719815aed320c95d340f464db) --- .../input/IInputDeviceBatteryListener.aidl | 29 +++ .../android/hardware/input/IInputManager.aidl | 5 + .../android/hardware/input/InputManager.java | 207 +++++++++++++++ core/java/android/view/InputDevice.java | 9 + .../input/InputDeviceBatteryListenerTest.kt | 227 +++++++++++++++++ .../server/input/BatteryController.java | 235 ++++++++++++++++++ .../server/input/InputManagerService.java | 21 +- .../server/input/BatteryControllerTests.kt | 169 +++++++++++++ 8 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 core/java/android/hardware/input/IInputDeviceBatteryListener.aidl create mode 100644 core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt create mode 100644 services/core/java/com/android/server/input/BatteryController.java create mode 100644 services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt diff --git a/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl new file mode 100644 index 0000000000000..dc5a96684606b --- /dev/null +++ b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** @hide */ +oneway interface IInputDeviceBatteryListener { + + /** + * Called when there is a change in battery state for a monitored device. This will be called + * immediately after the listener is successfully registered for a new device via IInputManager. + * The parameters are values exposed through {@link android.hardware.BatteryState}. + */ + void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, float capacity, + long eventTime); +} diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 2da12e6c5c9d8..a645ae4a3964e 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.KeyboardLayout; import android.hardware.input.IInputDevicesChangedListener; +import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.TouchCalibration; import android.os.CombinedVibration; @@ -155,4 +156,8 @@ interface IInputManager { void closeLightSession(int deviceId, in IBinder token); void cancelCurrentTouch(); + + void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener); + + void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index d17a9523ab37d..97812cea23e18 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -30,6 +30,7 @@ import android.app.ActivityThread; import android.compat.annotation.ChangeId; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.hardware.BatteryState; import android.hardware.SensorManager; import android.hardware.lights.Light; import android.hardware.lights.LightState; @@ -66,6 +67,7 @@ import android.view.PointerIcon; import android.view.VerifiedInputEvent; import android.view.WindowManager.LayoutParams; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.util.ArrayUtils; @@ -74,6 +76,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executor; /** * Provides information about input devices and available key layouts. @@ -111,6 +115,14 @@ public final class InputManager { private TabletModeChangedListener mTabletModeChangedListener; private List mOnTabletModeChangedListeners; + private final Object mBatteryListenersLock = new Object(); + // Maps a deviceId whose battery is currently being monitored to an entry containing the + // registered listeners for that device. + @GuardedBy("mBatteryListenersLock") + private SparseArray mBatteryListeners; + @GuardedBy("mBatteryListenersLock") + private IInputDeviceBatteryListener mInputDeviceBatteryListener; + private InputDeviceSensorManager mInputDeviceSensorManager; /** * Broadcast Action: Query available keyboard layouts. @@ -1751,6 +1763,129 @@ public final class InputManager { } } + /** + * Adds a battery listener to be notified about {@link BatteryState} changes for an input + * device. The same listener can be registered for multiple input devices. + * The listener will be notified of the initial battery state of the device after it is + * successfully registered. + * @param deviceId the input device that should be monitored + * @param executor an executor on which the callback will be called + * @param listener the {@link InputDeviceBatteryListener} + * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener) + * @hide + */ + public void addInputDeviceBatteryListener(int deviceId, @NonNull Executor executor, + @NonNull InputDeviceBatteryListener listener) { + Objects.requireNonNull(executor, "executor should not be null"); + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mBatteryListenersLock) { + if (mBatteryListeners == null) { + mBatteryListeners = new SparseArray<>(); + mInputDeviceBatteryListener = new LocalInputDeviceBatteryListener(); + } + RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId); + if (listenersForDevice == null) { + // The deviceId is currently not being monitored for battery changes. + // Start monitoring the device. + listenersForDevice = new RegisteredBatteryListeners(); + mBatteryListeners.put(deviceId, listenersForDevice); + try { + mIm.registerBatteryListener(deviceId, mInputDeviceBatteryListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } else { + // The deviceId is already being monitored for battery changes. + // Ensure that the listener is not already registered. + for (InputDeviceBatteryListenerDelegate delegate : listenersForDevice.mDelegates) { + if (Objects.equals(listener, delegate.mListener)) { + throw new IllegalArgumentException( + "Attempting to register an InputDeviceBatteryListener that has " + + "already been registered for deviceId: " + + deviceId); + } + } + } + final InputDeviceBatteryListenerDelegate delegate = + new InputDeviceBatteryListenerDelegate(listener, executor); + listenersForDevice.mDelegates.add(delegate); + + // Notify the listener immediately if we already have the latest battery state. + if (listenersForDevice.mLatestBatteryState != null) { + delegate.notifyBatteryStateChanged(listenersForDevice.mLatestBatteryState); + } + } + } + + /** + * Removes a previously registered battery listener for an input device. + * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener) + * @hide + */ + public void removeInputDeviceBatteryListener(int deviceId, + @NonNull InputDeviceBatteryListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mBatteryListenersLock) { + if (mBatteryListeners == null) { + return; + } + RegisteredBatteryListeners listenersForDevice = mBatteryListeners.get(deviceId); + if (listenersForDevice == null) { + // The deviceId is not currently being monitored. + return; + } + final List delegates = + listenersForDevice.mDelegates; + for (int i = 0; i < delegates.size();) { + if (Objects.equals(listener, delegates.get(i).mListener)) { + delegates.remove(i); + continue; + } + i++; + } + if (!delegates.isEmpty()) { + return; + } + + // There are no more battery listeners for this deviceId. Stop monitoring this device. + mBatteryListeners.remove(deviceId); + try { + mIm.unregisterBatteryListener(deviceId, mInputDeviceBatteryListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + if (mBatteryListeners.size() == 0) { + // There are no more devices being monitored, so the registered + // IInputDeviceBatteryListener will be automatically dropped by the server. + mBatteryListeners = null; + mInputDeviceBatteryListener = null; + } + } + } + + /** + * A callback used to be notified about battery state changes for an input device. The + * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the + * listener is successfully registered to provide the initial battery state of the device. + * @see InputDevice#getBatteryState() + * @see #addInputDeviceBatteryListener(int, Executor, InputDeviceBatteryListener) + * @see #removeInputDeviceBatteryListener(int, InputDeviceBatteryListener) + * @hide + */ + public interface InputDeviceBatteryListener { + /** + * Called when the battery state of an input device changes. + * @param deviceId the input device for which the battery changed. + * @param eventTimeMillis the time (in ms) when the battery change took place. + * This timestamp is in the {@link SystemClock#uptimeMillis()} time base. + * @param batteryState the new battery state, never null. + */ + void onBatteryStateChanged( + int deviceId, long eventTimeMillis, @NonNull BatteryState batteryState); + } + /** * Listens for changes in input devices. */ @@ -1861,4 +1996,76 @@ public final class InputManager { } } } + + private static final class LocalBatteryState extends BatteryState { + final int mDeviceId; + final boolean mIsPresent; + final int mStatus; + final float mCapacity; + final long mEventTime; + + LocalBatteryState(int deviceId, boolean isPresent, int status, float capacity, + long eventTime) { + mDeviceId = deviceId; + mIsPresent = isPresent; + mStatus = status; + mCapacity = capacity; + mEventTime = eventTime; + } + + @Override + public boolean isPresent() { + return mIsPresent; + } + + @Override + public int getStatus() { + return mStatus; + } + + @Override + public float getCapacity() { + return mCapacity; + } + } + + private static final class RegisteredBatteryListeners { + final List mDelegates = new ArrayList<>(); + LocalBatteryState mLatestBatteryState; + } + + private static final class InputDeviceBatteryListenerDelegate { + final InputDeviceBatteryListener mListener; + final Executor mExecutor; + + InputDeviceBatteryListenerDelegate(InputDeviceBatteryListener listener, Executor executor) { + mListener = listener; + mExecutor = executor; + } + + void notifyBatteryStateChanged(LocalBatteryState batteryState) { + mExecutor.execute(() -> + mListener.onBatteryStateChanged(batteryState.mDeviceId, batteryState.mEventTime, + batteryState)); + } + } + + private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub { + @Override + public void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, + float capacity, long eventTime) { + synchronized (mBatteryListenersLock) { + if (mBatteryListeners == null) return; + final RegisteredBatteryListeners entry = mBatteryListeners.get(deviceId); + if (entry == null) return; + + entry.mLatestBatteryState = + new LocalBatteryState( + deviceId, isBatteryPresent, status, capacity, eventTime); + for (InputDeviceBatteryListenerDelegate delegate : entry.mDelegates) { + delegate.notifyBatteryStateChanged(entry.mLatestBatteryState); + } + } + } + } } diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 7d5603994efaf..71644120cb43b 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -1021,6 +1021,15 @@ public final class InputDevice implements Parcelable { InputManager.getInstance().setCustomPointerIcon(icon); } + /** + * Reports whether the device has a battery. + * @return true if the device has a battery, false otherwise. + * @hide + */ + public boolean hasBattery() { + return mHasBattery; + } + /** * Provides information about the range of values for a particular {@link MotionEvent} axis. * diff --git a/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt new file mode 100644 index 0000000000000..e3b3ea7492c5b --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.hardware.input + +import android.hardware.BatteryState +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import com.android.server.testutils.any +import java.util.concurrent.Executor +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doAnswer +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner + +/** + * Tests for [InputManager.InputDeviceBatteryListener]. + * + * Build/Install/Run: + * atest FrameworksCoreTests:InputDeviceBatteryListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class InputDeviceBatteryListenerTest { + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var testLooper: TestLooper + private var registeredListener: IInputDeviceBatteryListener? = null + private val monitoredDevices = mutableListOf() + private lateinit var executor: Executor + private lateinit var inputManager: InputManager + + @Mock + private lateinit var iInputManagerMock: IInputManager + + @Before + fun setUp() { + testLooper = TestLooper() + executor = HandlerExecutor(Handler(testLooper.looper)) + registeredListener = null + monitoredDevices.clear() + inputManager = InputManager.resetInstance(iInputManagerMock) + + // Handle battery listener registration. + doAnswer { + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered battery listener per process. + fail("Trying to register a new listener when one already exists") + } + if (monitoredDevices.contains(deviceId)) { + fail("Trying to start monitoring a device that was already being monitored") + } + monitoredDevices.add(deviceId) + registeredListener = listener + null + }.`when`(iInputManagerMock).registerBatteryListener(anyInt(), any()) + + // Handle battery listener being unregistered. + doAnswer { + val deviceId = it.getArgument(0) as Int + val listener = it.getArgument(1) as IInputDeviceBatteryListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + if (!monitoredDevices.remove(deviceId)) { + fail("Trying to stop monitoring a device that is not being monitored") + } + if (monitoredDevices.isEmpty()) { + registeredListener = null + } + }.`when`(iInputManagerMock).unregisterBatteryListener(anyInt(), any()) + } + + @After + fun tearDown() { + InputManager.clearInstance() + } + + private fun notifyBatteryStateChanged( + deviceId: Int, + isPresent: Boolean = true, + status: Int = BatteryState.STATUS_FULL, + capacity: Float = 1.0f, + eventTime: Long = 12345L + ) { + registeredListener!!.onBatteryStateChanged(deviceId, isPresent, status, capacity, eventTime) + } + + @Test + fun testListenerIsNotifiedCorrectly() { + var callbackCount = 0 + + // Add a battery listener to monitor battery changes. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor) { + deviceId: Int, eventTime: Long, batteryState: BatteryState -> + callbackCount++ + assertEquals(1, deviceId) + assertEquals(true, batteryState.isPresent) + assertEquals(BatteryState.STATUS_DISCHARGING, batteryState.status) + assertEquals(0.5f, batteryState.capacity) + assertEquals(8675309L, eventTime) + } + + // Adding the listener should register the callback with InputManagerService. + assertNotNull(registeredListener) + assertTrue(monitoredDevices.contains(1)) + + // Notifying battery change for a different device should not trigger the listener. + notifyBatteryStateChanged(deviceId = 2) + testLooper.dispatchAll() + assertEquals(0, callbackCount) + + // Notifying battery change for the registered device will notify the listener. + notifyBatteryStateChanged(1 /*deviceId*/, true /*isPresent*/, + BatteryState.STATUS_DISCHARGING, 0.5f /*capacity*/, 8675309L /*eventTime*/) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ } + + // Monitor battery changes for three devices. The first callback monitors devices 1 and 3, + // while the second callback monitors devices 2 and 3. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1) + assertEquals(1, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(2 /*deviceId*/, executor, callback2) + assertEquals(2, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback1) + assertEquals(3, monitoredDevices.size) + inputManager.addInputDeviceBatteryListener(3 /*deviceId*/, executor, callback2) + assertEquals(3, monitoredDevices.size) + + // Notifying battery change for each of the devices should trigger the registered callbacks. + notifyBatteryStateChanged(deviceId = 1) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + assertEquals(0, callbackCount2) + + notifyBatteryStateChanged(deviceId = 2) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + notifyBatteryStateChanged(deviceId = 3) + testLooper.dispatchNext() + testLooper.dispatchNext() + assertEquals(2, callbackCount1) + assertEquals(2, callbackCount2) + + // Stop monitoring devices 1 and 2. + inputManager.removeInputDeviceBatteryListener(1 /*deviceId*/, callback1) + assertEquals(2, monitoredDevices.size) + inputManager.removeInputDeviceBatteryListener(2 /*deviceId*/, callback2) + assertEquals(1, monitoredDevices.size) + + // Ensure device 3 continues to be monitored. + notifyBatteryStateChanged(deviceId = 3) + testLooper.dispatchNext() + testLooper.dispatchNext() + assertEquals(3, callbackCount1) + assertEquals(3, callbackCount2) + + // Stop monitoring all devices. + inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback1) + assertEquals(1, monitoredDevices.size) + inputManager.removeInputDeviceBatteryListener(3 /*deviceId*/, callback2) + assertEquals(0, monitoredDevices.size) + } + + @Test + fun testAdditionalListenersNotifiedImmediately() { + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.InputDeviceBatteryListener { _, _, _ -> callbackCount2++ } + + // Add a battery listener and send the latest battery state. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback1) + assertEquals(1, monitoredDevices.size) + notifyBatteryStateChanged(deviceId = 1) + testLooper.dispatchNext() + assertEquals(1, callbackCount1) + + // Add a second listener for the same device that already has the latest battery state. + inputManager.addInputDeviceBatteryListener(1 /*deviceId*/, executor, callback2) + assertEquals(1, monitoredDevices.size) + + // Ensure that this listener is notified immediately. + testLooper.dispatchNext() + assertEquals(1, callbackCount2) + } +} diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java new file mode 100644 index 0000000000000..c57d7e79c12a7 --- /dev/null +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.annotation.BinderThread; +import android.annotation.NonNull; +import android.content.Context; +import android.hardware.input.IInputDeviceBatteryListener; +import android.hardware.input.InputManager; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.Slog; +import android.view.InputDevice; + +import com.android.internal.annotations.GuardedBy; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Objects; +import java.util.Set; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing the battery state + * of input devices. + */ +final class BatteryController { + private static final String TAG = BatteryController.class.getSimpleName(); + + // To enable these logs, run: + // 'adb shell setprop log.tag.BatteryController DEBUG' (requires restart) + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Object mLock = new Object(); + private final Context mContext; + private final NativeInputManagerService mNative; + + // 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) { + mContext = context; + mNative = nativeService; + } + + /** + * Register the battery listener for the given input device and start monitoring its battery + * state. + */ + @BinderThread + void registerBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, + int pid) { + synchronized (mLock) { + ListenerRecord listenerRecord = mListenerRecords.get(pid); + + if (listenerRecord == null) { + listenerRecord = new ListenerRecord(pid, listener); + try { + listener.asBinder().linkToDeath(listenerRecord.mDeathRecipient, 0); + } catch (RemoteException e) { + Slog.i(TAG, "Client died before battery listener could be registered."); + return; + } + mListenerRecords.put(pid, listenerRecord); + if (DEBUG) Slog.d(TAG, "Battery listener added for pid " + pid); + } + + if (listenerRecord.mListener.asBinder() != listener.asBinder()) { + throw new SecurityException( + "Cannot register a new battery listener when there is already another " + + "registered listener for pid " + + pid); + } + if (!listenerRecord.mMonitoredDevices.add(deviceId)) { + throw new IllegalArgumentException( + "The battery listener for pid " + pid + + " is already monitoring deviceId " + deviceId); + } + + if (DEBUG) { + Slog.d(TAG, "Battery listener for pid " + pid + + " is monitoring deviceId " + deviceId); + } + + notifyBatteryListener(deviceId, listenerRecord); + } + } + + private void notifyBatteryListener(int deviceId, ListenerRecord record) { + final long eventTime = SystemClock.uptimeMillis(); + try { + record.mListener.onBatteryStateChanged( + deviceId, + hasBattery(deviceId), + mNative.getBatteryStatus(deviceId), + mNative.getBatteryCapacity(deviceId) / 100.f, + eventTime); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to notify listener", e); + } + } + + private boolean hasBattery(int deviceId) { + final InputDevice device = + Objects.requireNonNull(mContext.getSystemService(InputManager.class)) + .getInputDevice(deviceId); + return device != null && device.hasBattery(); + } + + /** + * 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 + * removed. + */ + @BinderThread + void unregisterBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, + int pid) { + synchronized (mLock) { + final ListenerRecord listenerRecord = mListenerRecords.get(pid); + if (listenerRecord == null) { + throw new IllegalArgumentException( + "Cannot unregister battery callback: No listener registered for pid " + + pid); + } + + if (listenerRecord.mListener.asBinder() != listener.asBinder()) { + throw new IllegalArgumentException( + "Cannot unregister battery callback: The listener is not the one that " + + "is registered for pid " + + pid); + } + + if (!listenerRecord.mMonitoredDevices.contains(deviceId)) { + throw new IllegalArgumentException( + "Cannot unregister battery callback: The device is not being " + + "monitored for deviceId " + deviceId); + } + + unregisterRecordLocked(listenerRecord, deviceId); + } + } + + @GuardedBy("mLock") + private void unregisterRecordLocked(ListenerRecord listenerRecord, int deviceId) { + final int pid = listenerRecord.mPid; + + if (!listenerRecord.mMonitoredDevices.remove(deviceId)) { + throw new IllegalStateException("Cannot unregister battery callback: The deviceId " + + deviceId + + " is not being monitored by pid " + + pid); + } + + if (listenerRecord.mMonitoredDevices.isEmpty()) { + // There are no more devices being monitored by this listener. + listenerRecord.mListener.asBinder().unlinkToDeath(listenerRecord.mDeathRecipient, 0); + mListenerRecords.remove(pid); + if (DEBUG) Slog.d(TAG, "Battery listener removed for pid " + pid); + } + } + + private void handleListeningProcessDied(int pid) { + synchronized (mLock) { + final ListenerRecord listenerRecord = mListenerRecords.get(pid); + if (listenerRecord == null) { + return; + } + if (DEBUG) { + Slog.d(TAG, + "Removing battery listener for pid " + pid + " because the process died"); + } + for (final int deviceId : listenerRecord.mMonitoredDevices) { + unregisterRecordLocked(listenerRecord, deviceId); + } + } + } + + void dump(PrintWriter pw, String prefix) { + synchronized (mLock) { + pw.println(prefix + TAG + ": " + mListenerRecords.size() + + " battery listeners"); + for (int i = 0; i < mListenerRecords.size(); i++) { + pw.println(prefix + " " + i + ": " + mListenerRecords.valueAt(i)); + } + } + } + + @SuppressWarnings("all") + void monitor() { + synchronized (mLock) { + return; + } + } + + // A record of a registered battery listener from one process. + private class ListenerRecord { + final int mPid; + final IInputDeviceBatteryListener mListener; + final IBinder.DeathRecipient mDeathRecipient; + // The set of deviceIds that are currently being monitored by this listener. + final Set mMonitoredDevices; + + ListenerRecord(int pid, IInputDeviceBatteryListener listener) { + mPid = pid; + mListener = listener; + mMonitoredDevices = new ArraySet<>(); + mDeathRecipient = () -> handleListeningProcessDied(pid); + } + + @Override + public String toString() { + return "pid=" + mPid + + ", monitored devices=" + Arrays.toString(mMonitoredDevices.toArray()); + } + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 72612a0468cdd..91d5698123e8f 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -47,6 +47,7 @@ import android.hardware.SensorPrivacyManager.Sensors; import android.hardware.SensorPrivacyManagerInternal; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayViewport; +import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; @@ -318,6 +319,9 @@ public class InputManagerService extends IInputManager.Stub @GuardedBy("mInputMonitors") final Map mInputMonitors = new HashMap<>(); + // Manages battery state for input devices. + private final BatteryController mBatteryController; + // Maximum number of milliseconds to wait for input event injection. private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000; @@ -425,6 +429,7 @@ public class InputManagerService extends IInputManager.Stub mContext = injector.getContext(); mHandler = new InputManagerHandler(injector.getLooper()); mNative = injector.getNativeService(this); + mBatteryController = new BatteryController(mContext, mNative); mUseDevInputEventForAudioJack = mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack); @@ -2673,6 +2678,18 @@ public class InputManagerService extends IInputManager.Stub mNative.cancelCurrentTouch(); } + @Override + public void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener) { + Objects.requireNonNull(listener); + mBatteryController.registerBatteryListener(deviceId, listener, Binder.getCallingPid()); + } + + @Override + public void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener) { + Objects.requireNonNull(listener); + mBatteryController.unregisterBatteryListener(deviceId, listener, Binder.getCallingPid()); + } + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; @@ -2686,7 +2703,8 @@ public class InputManagerService extends IInputManager.Stub pw.println("Input Manager Service (Java) State:"); dumpAssociations(pw, " " /*prefix*/); dumpSpyWindowGestureMonitors(pw, " " /*prefix*/); - dumpDisplayInputPropertiesValues(pw, " " /* prefix */); + dumpDisplayInputPropertiesValues(pw, " " /*prefix*/); + mBatteryController.dump(pw, " " /*prefix*/); } private void dumpAssociations(PrintWriter pw, String prefix) { @@ -2797,6 +2815,7 @@ public class InputManagerService extends IInputManager.Stub synchronized (mLidSwitchLock) { /* Test if blocked by lid switch lock. */ } synchronized (mInputMonitors) { /* Test if blocked by input monitor lock. */ } synchronized (mAdditionalDisplayInputPropertiesLock) { /* Test if blocked by props lock */ } + mBatteryController.monitor(); mNative.monitor(); } diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt new file mode 100644 index 0000000000000..246aa90ea1733 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.BatteryState.STATUS_CHARGING +import android.hardware.BatteryState.STATUS_FULL +import android.hardware.input.IInputDeviceBatteryListener +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.os.Binder +import android.os.IBinder +import android.platform.test.annotations.Presubmit +import android.view.InputDevice +import androidx.test.InstrumentationRegistry +import org.junit.After +import org.junit.Assert.fail +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.notNull +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.anyLong +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.`when` +import org.mockito.junit.MockitoJUnit + +/** + * Tests for {@link InputDeviceBatteryController}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:InputDeviceBatteryControllerTests + */ +@Presubmit +class BatteryControllerTests { + companion object { + const val PID = 42 + const val DEVICE_ID = 13 + const val SECOND_DEVICE_ID = 11 + } + + @get:Rule + val rule = MockitoJUnit.rule()!! + + @Mock + private lateinit var native: NativeInputManagerService + @Mock + private lateinit var iInputManager: IInputManager + + private lateinit var batteryController: BatteryController + private lateinit var context: Context + + @Before + fun setup() { + context = spy(ContextWrapper(InstrumentationRegistry.getContext())) + 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)) + + batteryController = BatteryController(context, native) + } + + private fun createInputDevice(deviceId: Int): InputDevice = + InputDevice(deviceId, 0 /*generation*/, 0 /*controllerNumber*/, + "Device $deviceId" /*name*/, 0 /*vendorId*/, 0 /*productId*/, "descriptor$deviceId", + true /*isExternal*/, 0 /*sources*/, 0 /*keyboardType*/, null /*keyCharacterMap*/, + false /*hasVibrator*/, false /*hasMicrophone*/, false /*hasButtonUnderPad*/, + false /*hasSensor*/, true /*hasBattery*/) + + @After + fun tearDown() { + 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() + // Ensure the binder lifecycle is tracked when registering a listener. + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).linkToDeath(notNull(), anyInt()) + batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener.asBinder(), times(1)).linkToDeath(notNull(), anyInt()) + + // Ensure the binder lifecycle stops being tracked when all devices stopped being monitored. + batteryController.unregisterBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener.asBinder(), never()).unlinkToDeath(notNull(), anyInt()) + batteryController.unregisterBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt()) + } + + @Test + fun testOneListenerPerProcess() { + val listener1 = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener1, PID) + verify(listener1.asBinder()).linkToDeath(notNull(), anyInt()) + + // If a second listener is added for the same process, a security exception is thrown. + val listener2 = createMockListener() + try { + batteryController.registerBatteryListener(DEVICE_ID, listener2, PID) + fail("Expected security exception when registering more than one listener per process") + } catch (ignored: SecurityException) { + } + } + + @Test + fun testProcessDeathRemovesListener() { + val deathRecipient = ArgumentCaptor.forClass(IBinder.DeathRecipient::class.java) + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder()).linkToDeath(deathRecipient.capture(), anyInt()) + + // When the binder dies, the callback is unregistered. + deathRecipient.value!!.binderDied(listener.asBinder()) + verify(listener.asBinder()).unlinkToDeath(notNull(), anyInt()) + + // It is now possible to register the same listener again. + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener.asBinder(), times(2)).linkToDeath(notNull(), anyInt()) + } + + @Test + fun testRegisteringListenerNotifiesStateImmediately() { + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_FULL) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(100) + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), + eq(STATUS_FULL), eq(1f), anyLong()) + + `when`(native.getBatteryStatus(SECOND_DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(SECOND_DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(SECOND_DEVICE_ID, listener, PID) + verify(listener).onBatteryStateChanged(eq(SECOND_DEVICE_ID), eq(true /*isPresent*/), + eq(STATUS_CHARGING), eq(0.78f), anyLong()) + } +}