diff --git a/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl index dc5a96684606b..89324352ca34d 100644 --- a/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl +++ b/core/java/android/hardware/input/IInputDeviceBatteryListener.aidl @@ -16,14 +16,14 @@ package android.hardware.input; +import android.hardware.input.IInputDeviceBatteryState; + /** @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); + void onBatteryStateChanged(in IInputDeviceBatteryState batteryState); } diff --git a/core/java/android/hardware/input/IInputDeviceBatteryState.aidl b/core/java/android/hardware/input/IInputDeviceBatteryState.aidl new file mode 100644 index 0000000000000..561286cbabd6e --- /dev/null +++ b/core/java/android/hardware/input/IInputDeviceBatteryState.aidl @@ -0,0 +1,40 @@ +/* + * 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 */ +@JavaDerive(equals=true) +parcelable IInputDeviceBatteryState { + /** The deviceId of the input device that this battery state is associated with. */ + int deviceId; + + /** + * The timestamp of the last time the battery state was updated, in the + * {@link SystemClock.uptimeMillis()} time base. + */ + long updateTime; + + /** Whether the input device has a battery. */ + boolean isPresent; + + /** The battery status for this input device. */ + @JavaPassthrough(annotation="@android.hardware.BatteryState.BatteryStatus") + int status; + + /** The battery capacity for this input device, in a range between 0 and 1. */ + float capacity; +} \ No newline at end of file diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 36297b9919120..f213224b981e1 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -21,6 +21,7 @@ import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.KeyboardLayout; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputDeviceBatteryListener; +import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.TouchCalibration; import android.os.CombinedVibration; @@ -110,9 +111,7 @@ interface IInputManager { boolean registerVibratorStateListener(int deviceId, in IVibratorStateListener listener); boolean unregisterVibratorStateListener(int deviceId, in IVibratorStateListener listener); - // Input device battery query. - int getBatteryStatus(int deviceId); - int getBatteryCapacity(int deviceId); + IInputDeviceBatteryState getBatteryState(int deviceId); void setPointerIconType(int typeId); void setCustomPointerIcon(in PointerIcon icon); diff --git a/core/java/android/hardware/input/InputDeviceBatteryState.java b/core/java/android/hardware/input/InputDeviceBatteryState.java deleted file mode 100644 index d069eb0ddb812..0000000000000 --- a/core/java/android/hardware/input/InputDeviceBatteryState.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2021 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 static android.os.BatteryManager.BATTERY_STATUS_UNKNOWN; -import static android.os.IInputConstants.INVALID_BATTERY_CAPACITY; - -import android.hardware.BatteryState; - -/** - * Battery implementation for input devices. - * - * @hide - */ -public final class InputDeviceBatteryState extends BatteryState { - private static final float NULL_BATTERY_CAPACITY = Float.NaN; - - private final InputManager mInputManager; - private final int mDeviceId; - private final boolean mHasBattery; - - InputDeviceBatteryState(InputManager inputManager, int deviceId, boolean hasBattery) { - mInputManager = inputManager; - mDeviceId = deviceId; - mHasBattery = hasBattery; - } - - @Override - public boolean isPresent() { - return mHasBattery; - } - - @Override - public int getStatus() { - if (!mHasBattery) { - return BATTERY_STATUS_UNKNOWN; - } - return mInputManager.getBatteryStatus(mDeviceId); - } - - @Override - public float getCapacity() { - if (mHasBattery) { - int capacity = mInputManager.getBatteryCapacity(mDeviceId); - if (capacity != INVALID_BATTERY_CAPACITY) { - return (float) capacity / 100.0f; - } - } - return NULL_BATTERY_CAPACITY; - } -} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 8960d2a336b15..8d4aac4bba885 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -1307,32 +1307,6 @@ public final class InputManager { } } - /** - * Get the battery status of the input device - * @param deviceId The input device ID - * @hide - */ - public int getBatteryStatus(int deviceId) { - try { - return mIm.getBatteryStatus(deviceId); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - - /** - * Get the remaining battery capacity of the input device - * @param deviceId The input device ID - * @hide - */ - public int getBatteryCapacity(int deviceId) { - try { - return mIm.getBatteryCapacity(deviceId); - } catch (RemoteException ex) { - throw ex.rethrowFromSystemServer(); - } - } - /** * Add a runtime association between the input port and the display port. This overrides any * static associations. @@ -1622,8 +1596,17 @@ public final class InputManager { * @return The battery, never null. * @hide */ - public InputDeviceBatteryState getInputDeviceBatteryState(int deviceId, boolean hasBattery) { - return new InputDeviceBatteryState(this, deviceId, hasBattery); + @NonNull + public BatteryState getInputDeviceBatteryState(int deviceId, boolean hasBattery) { + if (!hasBattery) { + return new LocalBatteryState(); + } + try { + final IInputDeviceBatteryState state = mIm.getBatteryState(deviceId); + return new LocalBatteryState(state.isPresent, state.status, state.capacity); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } } /** @@ -1767,8 +1750,8 @@ public final class InputManager { listenersForDevice.mDelegates.add(delegate); // Notify the listener immediately if we already have the latest battery state. - if (listenersForDevice.mLatestBatteryState != null) { - delegate.notifyBatteryStateChanged(listenersForDevice.mLatestBatteryState); + if (listenersForDevice.mInputDeviceBatteryState != null) { + delegate.notifyBatteryStateChanged(listenersForDevice.mInputDeviceBatteryState); } } } @@ -1952,20 +1935,21 @@ public final class InputManager { } } + // Implementation of the android.hardware.BatteryState interface used to report the battery + // state via the InputDevice#getBatteryState() and InputDeviceBatteryListener interfaces. private static final class LocalBatteryState extends BatteryState { - final int mDeviceId; - final boolean mIsPresent; - final int mStatus; - final float mCapacity; - final long mEventTime; + private final boolean mIsPresent; + private final int mStatus; + private final float mCapacity; - LocalBatteryState(int deviceId, boolean isPresent, int status, float capacity, - long eventTime) { - mDeviceId = deviceId; + LocalBatteryState() { + this(false /*isPresent*/, BatteryState.STATUS_UNKNOWN, Float.NaN /*capacity*/); + } + + LocalBatteryState(boolean isPresent, int status, float capacity) { mIsPresent = isPresent; mStatus = status; mCapacity = capacity; - mEventTime = eventTime; } @Override @@ -1986,7 +1970,7 @@ public final class InputManager { private static final class RegisteredBatteryListeners { final List mDelegates = new ArrayList<>(); - LocalBatteryState mLatestBatteryState; + IInputDeviceBatteryState mInputDeviceBatteryState; } private static final class InputDeviceBatteryListenerDelegate { @@ -1998,27 +1982,24 @@ public final class InputManager { mExecutor = executor; } - void notifyBatteryStateChanged(LocalBatteryState batteryState) { + void notifyBatteryStateChanged(IInputDeviceBatteryState state) { mExecutor.execute(() -> - mListener.onBatteryStateChanged(batteryState.mDeviceId, batteryState.mEventTime, - batteryState)); + mListener.onBatteryStateChanged(state.deviceId, state.updateTime, + new LocalBatteryState(state.isPresent, state.status, state.capacity))); } } private class LocalInputDeviceBatteryListener extends IInputDeviceBatteryListener.Stub { @Override - public void onBatteryStateChanged(int deviceId, boolean isBatteryPresent, int status, - float capacity, long eventTime) { + public void onBatteryStateChanged(IInputDeviceBatteryState state) { synchronized (mBatteryListenersLock) { if (mBatteryListeners == null) return; - final RegisteredBatteryListeners entry = mBatteryListeners.get(deviceId); + final RegisteredBatteryListeners entry = mBatteryListeners.get(state.deviceId); if (entry == null) return; - entry.mLatestBatteryState = - new LocalBatteryState( - deviceId, isBatteryPresent, status, capacity, eventTime); + entry.mInputDeviceBatteryState = state; for (InputDeviceBatteryListenerDelegate delegate : entry.mDelegates) { - delegate.notifyBatteryStateChanged(entry.mLatestBatteryState); + delegate.notifyBatteryStateChanged(entry.mInputDeviceBatteryState); } } } diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 42c37fd97213f..9fd8ecb1727f9 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -91,9 +91,6 @@ public final class InputDevice implements Parcelable { @GuardedBy("mMotionRanges") private SensorManager mSensorManager; - @GuardedBy("mMotionRanges") - private BatteryState mBatteryState; - @GuardedBy("mMotionRanges") private LightsManager mLightsManager; @@ -1058,10 +1055,7 @@ public final class InputDevice implements Parcelable { */ @NonNull public BatteryState getBatteryState() { - if (mBatteryState == null) { - mBatteryState = InputManager.getInstance().getInputDeviceBatteryState(mId, mHasBattery); - } - return mBatteryState; + return InputManager.getInstance().getInputDeviceBatteryState(mId, mHasBattery); } /** diff --git a/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt index e3b3ea7492c5b..4f27e9997cf62 100644 --- a/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt +++ b/core/tests/coretests/src/android/hardware/input/InputDeviceBatteryListenerTest.kt @@ -112,7 +112,13 @@ class InputDeviceBatteryListenerTest { capacity: Float = 1.0f, eventTime: Long = 12345L ) { - registeredListener!!.onBatteryStateChanged(deviceId, isPresent, status, capacity, eventTime) + registeredListener!!.onBatteryStateChanged(IInputDeviceBatteryState().apply { + this.deviceId = deviceId + this.updateTime = eventTime + this.isPresent = isPresent + this.status = status + this.capacity = capacity + }) } @Test diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index 32f17dc18033a..696b6047137a6 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.BatteryState; import android.hardware.input.IInputDeviceBatteryListener; +import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.InputManager; import android.os.Handler; import android.os.IBinder; @@ -47,8 +48,13 @@ import java.util.Set; /** * A thread-safe component of {@link InputManagerService} responsible for managing the battery state * of input devices. + * + * Interactions with BatteryController can happen on several threads, including Binder threads, the + * {@link UEventObserver}'s thread, or its own Handler thread, among others. All public methods, and + * private methods prefixed with "handle-" (e.g. {@link #handleListeningProcessDied(int)}), + * serve as entry points for these threads. */ -final class BatteryController implements InputManager.InputDeviceListener { +final class BatteryController { private static final String TAG = BatteryController.class.getSimpleName(); // To enable these logs, run: @@ -69,10 +75,9 @@ final class BatteryController implements InputManager.InputDeviceListener { @GuardedBy("mLock") private final ArrayMap mListenerRecords = new ArrayMap<>(); - // Maps a deviceId that is being monitored to the battery state for the device. - // This must be kept in sync with {@link #mListenerRecords}. + // Maps a deviceId that is being monitored to the monitor for the battery state of the device. @GuardedBy("mLock") - private final ArrayMap mMonitoredDeviceStates = new ArrayMap<>(); + private final ArrayMap mDeviceMonitors = new ArrayMap<>(); @GuardedBy("mLock") private boolean mIsPolling = false; @@ -92,9 +97,9 @@ final class BatteryController implements InputManager.InputDeviceListener { mUEventManager = uEventManager; } - void systemRunning() { + public void systemRunning() { Objects.requireNonNull(mContext.getSystemService(InputManager.class)) - .registerInputDeviceListener(this, mHandler); + .registerInputDeviceListener(mInputDeviceListener, mHandler); } /** @@ -102,7 +107,7 @@ final class BatteryController implements InputManager.InputDeviceListener { * state. */ @BinderThread - void registerBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, + public void registerBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, int pid) { synchronized (mLock) { ListenerRecord listenerRecord = mListenerRecords.get(pid); @@ -131,11 +136,11 @@ final class BatteryController implements InputManager.InputDeviceListener { + " is already monitoring deviceId " + deviceId); } - MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId); - if (deviceState == null) { + DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { // This is the first listener that is monitoring this device. - deviceState = new MonitoredDeviceState(deviceId); - mMonitoredDeviceStates.put(deviceId, deviceState); + monitor = new DeviceMonitor(deviceId); + mDeviceMonitors.put(deviceId, monitor); } if (DEBUG) { @@ -144,36 +149,35 @@ final class BatteryController implements InputManager.InputDeviceListener { } updatePollingLocked(true /*delayStart*/); - notifyBatteryListener(listenerRecord, deviceState); + notifyBatteryListener(listenerRecord, monitor.getBatteryStateForReporting()); } } - private static void notifyBatteryListener(ListenerRecord listenerRecord, - MonitoredDeviceState deviceState) { + private static void notifyBatteryListener(ListenerRecord listenerRecord, State state) { try { - listenerRecord.mListener.onBatteryStateChanged( - deviceState.mDeviceId, - deviceState.mHasBattery, - deviceState.mBatteryStatus, - deviceState.mBatteryCapacity, - deviceState.mLastUpdateTime); + listenerRecord.mListener.onBatteryStateChanged(state); } catch (RemoteException e) { Slog.e(TAG, "Failed to notify listener", e); } + if (DEBUG) { + Slog.d(TAG, "Notified battery listener from pid " + listenerRecord.mPid + + " of state of deviceId " + state.deviceId); + } } @GuardedBy("mLock") - private void notifyAllListenersForDeviceLocked(MonitoredDeviceState deviceState) { + private void notifyAllListenersForDeviceLocked(State state) { + if (DEBUG) Slog.d(TAG, "Notifying all listeners of battery state: " + state); mListenerRecords.forEach((pid, listenerRecord) -> { - if (listenerRecord.mMonitoredDevices.contains(deviceState.mDeviceId)) { - notifyBatteryListener(listenerRecord, deviceState); + if (listenerRecord.mMonitoredDevices.contains(state.deviceId)) { + notifyBatteryListener(listenerRecord, state); } }); } @GuardedBy("mLock") private void updatePollingLocked(boolean delayStart) { - if (mMonitoredDeviceStates.isEmpty() || !mIsInteractive) { + if (mDeviceMonitors.isEmpty() || !mIsInteractive) { // Stop polling. mIsPolling = false; mHandler.removeCallbacks(this::handlePollEvent); @@ -196,8 +200,8 @@ final class BatteryController implements InputManager.InputDeviceListener { } @GuardedBy("mLock") - private MonitoredDeviceState getDeviceStateOrThrowLocked(int deviceId) { - return Objects.requireNonNull(mMonitoredDeviceStates.get(deviceId), + private DeviceMonitor getDeviceMonitorOrThrowLocked(int deviceId) { + return Objects.requireNonNull(mDeviceMonitors.get(deviceId), "Maps are out of sync: Cannot find device state for deviceId " + deviceId); } @@ -207,8 +211,8 @@ final class BatteryController implements InputManager.InputDeviceListener { * removed. */ @BinderThread - void unregisterBatteryListener(int deviceId, @NonNull IInputDeviceBatteryListener listener, - int pid) { + public void unregisterBatteryListener(int deviceId, + @NonNull IInputDeviceBatteryListener listener, int pid) { synchronized (mLock) { final ListenerRecord listenerRecord = mListenerRecords.get(pid); if (listenerRecord == null) { @@ -247,9 +251,9 @@ final class BatteryController implements InputManager.InputDeviceListener { if (!hasRegisteredListenerForDeviceLocked(deviceId)) { // There are no more listeners monitoring this device. - final MonitoredDeviceState deviceState = getDeviceStateOrThrowLocked(deviceId); - deviceState.stopMonitoring(); - mMonitoredDeviceStates.remove(deviceId); + final DeviceMonitor monitor = getDeviceMonitorOrThrowLocked(deviceId); + monitor.stopMonitoring(); + mDeviceMonitors.remove(deviceId); } if (listenerRecord.mMonitoredDevices.isEmpty()) { @@ -288,15 +292,14 @@ final class BatteryController implements InputManager.InputDeviceListener { } } - // Query the battery state for the device and notify all listeners if there is a change. - private void handleBatteryChangeNotification(int deviceId, long eventTime) { + private void handleUEventNotification(int deviceId, long eventTime) { synchronized (mLock) { - final MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId); - if (deviceState == null) { + final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { return; } - if (deviceState.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(deviceState); + if (monitor.updateBatteryState(eventTime)) { + notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); } } } @@ -307,11 +310,11 @@ final class BatteryController implements InputManager.InputDeviceListener { return; } final long eventTime = SystemClock.uptimeMillis(); - mMonitoredDeviceStates.forEach((deviceId, deviceState) -> { + mDeviceMonitors.forEach((deviceId, monitor) -> { // Re-acquire lock in the lambda to silence error-prone build warnings. synchronized (mLock) { - if (deviceState.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(deviceState); + if (monitor.updateBatteryState(eventTime)) { + notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); } } }); @@ -319,62 +322,90 @@ final class BatteryController implements InputManager.InputDeviceListener { } } - void onInteractiveChanged(boolean interactive) { + /** Gets the current battery state of an input device. */ + public IInputDeviceBatteryState getBatteryState(int deviceId) { + synchronized (mLock) { + final long updateTime = SystemClock.uptimeMillis(); + final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { + // The input device's battery is not being monitored by any listener. + return queryBatteryStateFromNative(deviceId, updateTime); + } + // Force the battery state to update, and notify listeners if necessary. + final boolean stateChanged = monitor.updateBatteryState(updateTime); + final State state = monitor.getBatteryStateForReporting(); + if (stateChanged) { + notifyAllListenersForDeviceLocked(state); + } + return state; + } + } + + public void onInteractiveChanged(boolean interactive) { synchronized (mLock) { mIsInteractive = interactive; updatePollingLocked(false /*delayStart*/); } } - void dump(PrintWriter pw, String prefix) { + public void dump(PrintWriter pw, String prefix) { synchronized (mLock) { - pw.println(prefix + TAG + ": " - + mListenerRecords.size() + " battery listeners" - + ", Polling = " + mIsPolling + final String indent = prefix + " "; + final String indent2 = indent + " "; + + pw.println(prefix + TAG + ":"); + pw.println(indent + "State: Polling = " + mIsPolling + ", Interactive = " + mIsInteractive); + + pw.println(indent + "Listeners: " + mListenerRecords.size() + " battery listeners"); for (int i = 0; i < mListenerRecords.size(); i++) { - pw.println(prefix + " " + i + ": " + mListenerRecords.valueAt(i)); + pw.println(indent2 + i + ": " + mListenerRecords.valueAt(i)); + } + + pw.println(indent + "Device Monitors: " + mDeviceMonitors.size() + " monitors"); + for (int i = 0; i < mDeviceMonitors.size(); i++) { + pw.println(indent2 + i + ": " + mDeviceMonitors.valueAt(i)); } } } @SuppressWarnings("all") - void monitor() { + public void monitor() { synchronized (mLock) { return; } } - @VisibleForTesting - @Override - public void onInputDeviceAdded(int deviceId) {} + private final InputManager.InputDeviceListener mInputDeviceListener = + new InputManager.InputDeviceListener() { + @Override + public void onInputDeviceAdded(int deviceId) {} - @VisibleForTesting - @Override - public void onInputDeviceRemoved(int deviceId) {} + @Override + public void onInputDeviceRemoved(int deviceId) {} - @VisibleForTesting - @Override - public void onInputDeviceChanged(int deviceId) { - synchronized (mLock) { - final MonitoredDeviceState deviceState = mMonitoredDeviceStates.get(deviceId); - if (deviceState == null) { - return; - } - final long eventTime = SystemClock.uptimeMillis(); - if (deviceState.updateBatteryState(eventTime)) { - notifyAllListenersForDeviceLocked(deviceState); + @Override + public void onInputDeviceChanged(int deviceId) { + synchronized (mLock) { + final DeviceMonitor monitor = mDeviceMonitors.get(deviceId); + if (monitor == null) { + return; + } + final long eventTime = SystemClock.uptimeMillis(); + if (monitor.updateBatteryState(eventTime)) { + notifyAllListenersForDeviceLocked(monitor.getBatteryStateForReporting()); + } } } - } + }; // A record of a registered battery listener from one process. private class ListenerRecord { - final int mPid; - final IInputDeviceBatteryListener mListener; - final IBinder.DeathRecipient mDeathRecipient; + public final int mPid; + public final IInputDeviceBatteryListener mListener; + public final IBinder.DeathRecipient mDeathRecipient; // The set of deviceIds that are currently being monitored by this listener. - final Set mMonitoredDevices; + public final Set mMonitoredDevices; ListenerRecord(int pid, IInputDeviceBatteryListener listener) { mPid = pid; @@ -390,21 +421,27 @@ final class BatteryController implements InputManager.InputDeviceListener { } } - // Holds the state of an InputDevice for which battery changes are currently being monitored. - private class MonitoredDeviceState { - private final int mDeviceId; + // Queries the battery state of an input device from native code. + private State queryBatteryStateFromNative(int deviceId, long updateTime) { + final boolean isPresent = hasBattery(deviceId); + return new State( + deviceId, + updateTime, + isPresent, + isPresent ? mNative.getBatteryStatus(deviceId) : BatteryState.STATUS_UNKNOWN, + isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); + } - private long mLastUpdateTime = 0; - private boolean mHasBattery = false; - @BatteryState.BatteryStatus - private int mBatteryStatus = BatteryState.STATUS_UNKNOWN; - private float mBatteryCapacity = Float.NaN; + // Holds the state of an InputDevice for which battery changes are currently being monitored. + private class DeviceMonitor { + @NonNull + private State mState; @Nullable private UEventListener mUEventListener; - MonitoredDeviceState(int deviceId) { - mDeviceId = deviceId; + DeviceMonitor(int deviceId) { + mState = new State(deviceId); // Load the initial battery state and start monitoring. final long eventTime = SystemClock.uptimeMillis(); @@ -412,56 +449,57 @@ final class BatteryController implements InputManager.InputDeviceListener { } // Returns true if the battery state changed since the last time it was updated. - boolean updateBatteryState(long eventTime) { - mLastUpdateTime = eventTime; + public boolean updateBatteryState(long updateTime) { + mState.updateTime = updateTime; - final boolean batteryPresenceChanged = mHasBattery != hasBattery(mDeviceId); - if (batteryPresenceChanged) { - mHasBattery = !mHasBattery; - if (mHasBattery) { + final State updatedState = queryBatteryStateFromNative(mState.deviceId, updateTime); + if (mState.equals(updatedState)) { + return false; + } + if (mState.isPresent != updatedState.isPresent) { + if (updatedState.isPresent) { startMonitoring(); } else { stopMonitoring(); } } - - final int oldStatus = mBatteryStatus; - final float oldCapacity = mBatteryCapacity; - - if (mHasBattery) { - mBatteryStatus = mNative.getBatteryStatus(mDeviceId); - mBatteryCapacity = mNative.getBatteryCapacity(mDeviceId) / 100.f; - } else { - mBatteryStatus = BatteryState.STATUS_UNKNOWN; - mBatteryCapacity = Float.NaN; - } - - return batteryPresenceChanged - || mBatteryStatus != oldStatus - || mBatteryCapacity != oldCapacity; + mState = updatedState; + return true; } private void startMonitoring() { - final String batteryPath = mNative.getBatteryDevicePath(mDeviceId); + final String batteryPath = mNative.getBatteryDevicePath(mState.deviceId); if (batteryPath == null) { return; } + final int deviceId = mState.deviceId; mUEventListener = new UEventListener() { @Override - void onUEvent(long eventTime) { - handleBatteryChangeNotification(mDeviceId, eventTime); + public void onUEvent(long eventTime) { + handleUEventNotification(deviceId, eventTime); } }; mUEventManager.addListener(mUEventListener, "DEVPATH=" + batteryPath); } // This must be called when the device is no longer being monitored. - void stopMonitoring() { + public void stopMonitoring() { if (mUEventListener != null) { mUEventManager.removeListener(mUEventListener); mUEventListener = null; } } + + // Returns the current battery state that can be used to notify listeners BatteryController. + public State getBatteryStateForReporting() { + return new State(mState); + } + + @Override + public String toString() { + return "state=" + mState + + ", uEventListener=" + (mUEventListener != null ? "added" : "none"); + } } // An interface used to change the API of UEventObserver to a more test-friendly format. @@ -483,7 +521,7 @@ final class BatteryController implements InputManager.InputDeviceListener { } }; - abstract void onUEvent(long eventTime); + public abstract void onUEvent(long eventTime); } default void addListener(UEventListener listener, String match) { @@ -494,4 +532,37 @@ final class BatteryController implements InputManager.InputDeviceListener { listener.mObserver.stopObserving(); } } + + // Helper class that adds copying and printing functionality to IInputDeviceBatteryState. + private static class State extends IInputDeviceBatteryState { + + State(int deviceId) { + initialize(deviceId, 0 /*updateTime*/, false /*isPresent*/, BatteryState.STATUS_UNKNOWN, + Float.NaN /*capacity*/); + } + + State(IInputDeviceBatteryState s) { + initialize(s.deviceId, s.updateTime, s.isPresent, s.status, s.capacity); + } + + State(int deviceId, long updateTime, boolean isPresent, int status, float capacity) { + initialize(deviceId, updateTime, isPresent, status, capacity); + } + + private void initialize(int deviceId, long updateTime, boolean isPresent, int status, + float capacity) { + this.deviceId = deviceId; + this.updateTime = updateTime; + this.isPresent = isPresent; + this.status = status; + this.capacity = capacity; + } + + @Override + public String toString() { + return "BatteryState{deviceId=" + deviceId + ", updateTime=" + updateTime + + ", isPresent=" + isPresent + ", status=" + status + ", capacity=" + capacity + + " }"; + } + } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 71feb952cb005..beb524ee1ed91 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -49,6 +49,7 @@ import android.hardware.SensorPrivacyManagerInternal; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayViewport; import android.hardware.input.IInputDeviceBatteryListener; +import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; @@ -2305,14 +2306,8 @@ public class InputManagerService extends IInputManager.Stub // Binder call @Override - public int getBatteryStatus(int deviceId) { - return mNative.getBatteryStatus(deviceId); - } - - // Binder call - @Override - public int getBatteryCapacity(int deviceId) { - return mNative.getBatteryCapacity(deviceId); + public IInputDeviceBatteryState getBatteryState(int deviceId) { + return mBatteryController.getBatteryState(deviceId); } // Binder call diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index 40757182890c8..5f3f3d7714ef7 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -22,6 +22,7 @@ import android.hardware.BatteryState.STATUS_CHARGING import android.hardware.BatteryState.STATUS_FULL import android.hardware.BatteryState.STATUS_UNKNOWN import android.hardware.input.IInputDeviceBatteryListener +import android.hardware.input.IInputDeviceBatteryState import android.hardware.input.IInputDevicesChangedListener import android.hardware.input.IInputManager import android.hardware.input.InputManager @@ -32,6 +33,12 @@ import android.platform.test.annotations.Presubmit import android.view.InputDevice import androidx.test.InstrumentationRegistry import com.android.server.input.BatteryController.UEventManager +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers +import org.hamcrest.TypeSafeMatcher +import org.hamcrest.core.IsEqual.equalTo import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.fail @@ -42,7 +49,6 @@ 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.clearInvocations import org.mockito.Mockito.eq import org.mockito.Mockito.mock @@ -52,7 +58,9 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` +import org.mockito.hamcrest.MockitoHamcrest import org.mockito.junit.MockitoJUnit +import org.mockito.verification.VerificationMode private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputDevice = InputDevice.Builder() @@ -64,6 +72,64 @@ private fun createInputDevice(deviceId: Int, hasBattery: Boolean = true): InputD .setGeneration(0) .build() +// Returns a matcher that helps match member variables of a class. +private fun memberMatcher( + member: String, + memberProvider: (T) -> U, + match: Matcher +): TypeSafeMatcher = + object : TypeSafeMatcher() { + + override fun matchesSafely(item: T?): Boolean { + return match.matches(memberProvider(item!!)) + } + + override fun describeMismatchSafely(item: T?, mismatchDescription: Description?) { + match.describeMismatch(item, mismatchDescription) + } + + override fun describeTo(description: Description?) { + match.describeTo(description?.appendText("matches member $member")) + } + } + +// Returns a matcher for IInputDeviceBatteryState that optionally matches some arguments. +private fun matchesState( + deviceId: Int, + isPresent: Boolean = true, + status: Int? = null, + capacity: Float? = null, + eventTime: Long? = null +): Matcher { + val batteryStateMatchers = mutableListOf>( + memberMatcher("deviceId", { it.deviceId }, equalTo(deviceId)), + memberMatcher("isPresent", { it.isPresent }, equalTo(isPresent)) + ) + if (eventTime != null) { + batteryStateMatchers.add(memberMatcher("updateTime", { it.updateTime }, equalTo(eventTime))) + } + if (status != null) { + batteryStateMatchers.add(memberMatcher("status", { it.status }, equalTo(status))) + } + if (capacity != null) { + batteryStateMatchers.add(memberMatcher("capacity", { it.capacity }, equalTo(capacity))) + } + return Matchers.allOf(batteryStateMatchers) +} + +// Helper used to verify interactions with a mocked battery listener. +private fun IInputDeviceBatteryListener.verifyNotified( + deviceId: Int, + mode: VerificationMode = times(1), + isPresent: Boolean = true, + status: Int? = null, + capacity: Float? = null, + eventTime: Long? = null +) { + verify(this, mode).onBatteryStateChanged( + MockitoHamcrest.argThat(matchesState(deviceId, isPresent, status, capacity, eventTime))) +} + /** * Tests for {@link InputDeviceBatteryController}. * @@ -184,14 +250,12 @@ class BatteryControllerTests { `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()) + listener.verifyNotified(DEVICE_ID, status = STATUS_FULL, capacity = 1.0f) `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()) + listener.verifyNotified(SECOND_DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) } @Test @@ -203,14 +267,13 @@ class BatteryControllerTests { val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java) batteryController.registerBatteryListener(DEVICE_ID, listener, PID) verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1")) - verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), - eq(STATUS_CHARGING), eq(0.78f), anyLong()) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) // If the battery state has changed when an UEvent is sent, the listeners are notified. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) uEventListener.value!!.onUEvent(TIMESTAMP) - verify(listener).onBatteryStateChanged(DEVICE_ID, true /*isPresent*/, STATUS_CHARGING, - 0.80f, TIMESTAMP) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f, + eventTime = TIMESTAMP) // If the battery state has not changed when an UEvent is sent, the listeners are not // notified. @@ -233,20 +296,15 @@ class BatteryControllerTests { val uEventListener = ArgumentCaptor.forClass(UEventManager.UEventListener::class.java) batteryController.registerBatteryListener(DEVICE_ID, listener, PID) verify(uEventManager).addListener(uEventListener.capture(), eq("DEVPATH=/test/device1")) - verify(listener).onBatteryStateChanged( - eq(DEVICE_ID), eq(true /*isPresent*/), - eq(STATUS_CHARGING), eq(0.78f), anyLong() - ) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) // If the battery presence for the InputDevice changes, the listener is notified. `when`(iInputManager.getInputDevice(DEVICE_ID)) .thenReturn(createInputDevice(DEVICE_ID, hasBattery = false)) notifyDeviceChanged(DEVICE_ID) testLooper.dispatchNext() - verify(listener).onBatteryStateChanged( - eq(DEVICE_ID), eq(false /*isPresent*/), - eq(STATUS_UNKNOWN), eq(Float.NaN), anyLong() - ) + listener.verifyNotified(DEVICE_ID, isPresent = false, status = STATUS_UNKNOWN, + capacity = Float.NaN) // Since the battery is no longer present, the UEventListener should be removed. verify(uEventManager).removeListener(uEventListener.value) @@ -255,36 +313,32 @@ class BatteryControllerTests { .thenReturn(createInputDevice(DEVICE_ID, hasBattery = true)) notifyDeviceChanged(DEVICE_ID) testLooper.dispatchNext() - verify(listener, times(2)).onBatteryStateChanged( - eq(DEVICE_ID), eq(true /*isPresent*/), - eq(STATUS_CHARGING), eq(0.78f), anyLong() - ) + listener.verifyNotified(DEVICE_ID, mode = times(2), status = STATUS_CHARGING, + capacity = 0.78f) // Ensure that a new UEventListener was added. verify(uEventManager, times(2)) .addListener(uEventListener.capture(), eq("DEVPATH=/test/device1")) } + @Test fun testStartPollingWhenListenerIsRegistered() { val listener = createMockListener() `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) batteryController.registerBatteryListener(DEVICE_ID, listener, PID) - verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), anyInt(), - eq(0.78f), anyLong()) + listener.verifyNotified(DEVICE_ID, capacity = 0.78f) // Assume there is a change in the battery state. Ensure the listener is not notified // while the polling period has not elapsed. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) testLooper.moveTimeForward(1) testLooper.dispatchAll() - verify(listener, never()).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), - anyInt(), eq(0.80f), anyLong()) + listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f) // Move the time forward so that the polling period has elapsed. // The listener should be notified. testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS - 1) testLooper.dispatchNext() - verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), anyInt(), - eq(0.80f), anyLong()) + listener.verifyNotified(DEVICE_ID, capacity = 0.80f) } @Test @@ -294,28 +348,50 @@ class BatteryControllerTests { val listener = createMockListener() `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) batteryController.registerBatteryListener(DEVICE_ID, listener, PID) - verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), anyInt(), - eq(0.78f), anyLong()) + listener.verifyNotified(DEVICE_ID, capacity = 0.78f) // The battery state changed, but we should not be polling for battery changes when the // device is not interactive. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS) testLooper.dispatchAll() - verify(listener, never()).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), - anyInt(), eq(0.80f), anyLong()) + listener.verifyNotified(DEVICE_ID, mode = never(), capacity = 0.80f) // The device is now interactive. Battery state polling begins immediately. batteryController.onInteractiveChanged(true /*interactive*/) testLooper.dispatchNext() - verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), - anyInt(), eq(0.80f), anyLong()) + listener.verifyNotified(DEVICE_ID, capacity = 0.80f) // Ensure that we continue to poll for battery changes. `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(90) testLooper.moveTimeForward(BatteryController.POLLING_PERIOD_MILLIS) testLooper.dispatchNext() - verify(listener).onBatteryStateChanged(eq(DEVICE_ID), eq(true /*isPresent*/), - anyInt(), eq(0.90f), anyLong()) + listener.verifyNotified(DEVICE_ID, capacity = 0.90f) + } + + @Test + fun testGetBatteryState() { + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + val batteryState = batteryController.getBatteryState(DEVICE_ID) + assertThat("battery state matches", batteryState, + matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f)) + } + + @Test + fun testGetBatteryStateWithListener() { + val listener = createMockListener() + `when`(native.getBatteryStatus(DEVICE_ID)).thenReturn(STATUS_CHARGING) + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(78) + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.78f) + + // If getBatteryState() is called when a listener is monitoring the device and there is a + // change in the battery state, the listener is also notified. + `when`(native.getBatteryCapacity(DEVICE_ID)).thenReturn(80) + val batteryState = batteryController.getBatteryState(DEVICE_ID) + assertThat("battery matches state", batteryState, + matchesState(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f)) + listener.verifyNotified(DEVICE_ID, status = STATUS_CHARGING, capacity = 0.80f) } }