Merge "Add InputDeviceBatteryListener to InputManager" into tm-qpr-dev

This commit is contained in:
TreeHugger Robot
2022-10-05 20:52:59 +00:00
committed by Android (Google) Code Review
8 changed files with 901 additions and 1 deletions

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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<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.
@@ -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<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.
*/
@@ -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<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);
}
}
}
}
}

View File

@@ -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.
*

View File

@@ -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)
}
}

View File

@@ -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());
}
}
}

View File

@@ -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<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;
@@ -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();
}

View File

@@ -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())
}
}