Loading core/java/android/hardware/input/IInputDeviceBatteryListener.aidl 0 → 100644 +29 −0 Original line number Diff line number Diff line /* * 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); } core/java/android/hardware/input/IInputManager.aidl +5 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -157,4 +158,8 @@ interface IInputManager { void closeLightSession(int deviceId, in IBinder token); void cancelCurrentTouch(); void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener); void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); } core/java/android/hardware/input/InputManager.java +207 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -65,6 +66,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; Loading @@ -72,6 +74,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. Loading Loading @@ -103,6 +107,14 @@ public final class InputManager { private TabletModeChangedListener mTabletModeChangedListener; private List<OnTabletModeChangedListenerDelegate> 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<RegisteredBatteryListeners> mBatteryListeners; @GuardedBy("mBatteryListenersLock") private IInputDeviceBatteryListener mInputDeviceBatteryListener; private InputDeviceSensorManager mInputDeviceSensorManager; /** * Broadcast Action: Query available keyboard layouts. Loading Loading @@ -1706,6 +1718,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<InputDeviceBatteryListenerDelegate> 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. */ Loading Loading @@ -1816,4 +1951,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<InputDeviceBatteryListenerDelegate> 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); } } } } } core/java/android/view/InputDevice.java +9 −0 Original line number Diff line number Diff line Loading @@ -1039,6 +1039,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. * Loading core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt 0 → 100644 +227 −0 Original line number Diff line number Diff line /* * 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<Int>() 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) } } Loading
core/java/android/hardware/input/IInputDeviceBatteryListener.aidl 0 → 100644 +29 −0 Original line number Diff line number Diff line /* * 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); }
core/java/android/hardware/input/IInputManager.aidl +5 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -157,4 +158,8 @@ interface IInputManager { void closeLightSession(int deviceId, in IBinder token); void cancelCurrentTouch(); void registerBatteryListener(int deviceId, IInputDeviceBatteryListener listener); void unregisterBatteryListener(int deviceId, IInputDeviceBatteryListener listener); }
core/java/android/hardware/input/InputManager.java +207 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -65,6 +66,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; Loading @@ -72,6 +74,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. Loading Loading @@ -103,6 +107,14 @@ public final class InputManager { private TabletModeChangedListener mTabletModeChangedListener; private List<OnTabletModeChangedListenerDelegate> 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<RegisteredBatteryListeners> mBatteryListeners; @GuardedBy("mBatteryListenersLock") private IInputDeviceBatteryListener mInputDeviceBatteryListener; private InputDeviceSensorManager mInputDeviceSensorManager; /** * Broadcast Action: Query available keyboard layouts. Loading Loading @@ -1706,6 +1718,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<InputDeviceBatteryListenerDelegate> 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. */ Loading Loading @@ -1816,4 +1951,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<InputDeviceBatteryListenerDelegate> 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); } } } } }
core/java/android/view/InputDevice.java +9 −0 Original line number Diff line number Diff line Loading @@ -1039,6 +1039,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. * Loading
core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt 0 → 100644 +227 −0 Original line number Diff line number Diff line /* * 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<Int>() 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) } }