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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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<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)
|
||||
}
|
||||
}
|
||||
@@ -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<Integer, ListenerRecord> 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<Integer> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,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;
|
||||
@@ -308,6 +309,9 @@ public class InputManagerService extends IInputManager.Stub
|
||||
@GuardedBy("mInputMonitors")
|
||||
final Map<IBinder, GestureMonitorSpyWindow> 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;
|
||||
|
||||
@@ -417,6 +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);
|
||||
|
||||
mUseDevInputEventForAudioJack =
|
||||
mContext.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);
|
||||
@@ -2652,6 +2657,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;
|
||||
@@ -2665,7 +2682,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) {
|
||||
@@ -2776,6 +2794,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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.InputDeviceCountryCode
|
||||
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*/,
|
||||
InputDeviceCountryCode.INVALID, 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user