From 95b9a3dd9ea19c82d2a134522f11f31782338fa2 Mon Sep 17 00:00:00 2001 From: Prabir Pradhan Date: Tue, 3 Jan 2023 23:36:47 +0000 Subject: [PATCH 1/2] BatteryController: Get Bluetooth battery level from callback Previously, when there was a Bluetooth battery update, we ignored the updated battery level from the broadcast's EXTRA_BATTERY_LEVEL and instead queried the BluetoothDevice for the battery level. In this CL, we change this behavior so that we obtain the battery level directly from the intent extra, removing the need to query the Bluetooth service again for the battery level. This refactor is to set up for getting battery level from the Bluetooth metadata in the following CL. Also, rename the BluetoothBatteryManager#add/removeListener() APIs to add/removeBatteryListener. Bug: 243005009 Test: atest BatteryControllerTests Change-Id: I24cd9fe10a3e2c6c400439a3cf891b1cda5f3a3b --- .../server/input/BatteryController.java | 95 +++++++++---------- .../server/input/BatteryControllerTests.kt | 36 ++++--- 2 files changed, 62 insertions(+), 69 deletions(-) diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index c99a7a0de79cb..b0c99c05c5831 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -378,13 +378,14 @@ final class BatteryController { } } - private void handleBluetoothBatteryLevelChange(long eventTime, String address) { + private void handleBluetoothBatteryLevelChange(long eventTime, String address, + int batteryLevel) { synchronized (mLock) { final DeviceMonitor monitor = findIf(mDeviceMonitors, (m) -> (m.mBluetoothDevice != null && address.equals(m.mBluetoothDevice.getAddress()))); if (monitor != null) { - monitor.onBluetoothBatteryChanged(eventTime); + monitor.onBluetoothBatteryChanged(eventTime, batteryLevel); } } } @@ -514,18 +515,6 @@ final class BatteryController { isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); } - // Queries the battery state of an input device from Bluetooth. - private State queryBatteryStateFromBluetooth(int deviceId, long updateTime, - @NonNull BluetoothDevice bluetoothDevice) { - final int level = mBluetoothBatteryManager.getBatteryLevel(bluetoothDevice.getAddress()); - if (level == BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF - || level == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { - return new State(deviceId); - } - return new State(deviceId, updateTime, true /*isPresent*/, BatteryState.STATUS_UNKNOWN, - level / 100.f); - } - private void updateBluetoothMonitoring() { synchronized (mLock) { if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { @@ -533,12 +522,12 @@ final class BatteryController { if (mBluetoothBatteryListener == null) { if (DEBUG) Slog.d(TAG, "Registering bluetooth battery listener"); mBluetoothBatteryListener = this::handleBluetoothBatteryLevelChange; - mBluetoothBatteryManager.addListener(mBluetoothBatteryListener); + mBluetoothBatteryManager.addBatteryListener(mBluetoothBatteryListener); } } else if (mBluetoothBatteryListener != null) { // No Bluetooth input devices are monitored, so remove the registered listener. if (DEBUG) Slog.d(TAG, "Unregistering bluetooth battery listener"); - mBluetoothBatteryManager.removeListener(mBluetoothBatteryListener); + mBluetoothBatteryManager.removeBatteryListener(mBluetoothBatteryListener); mBluetoothBatteryListener = null; } } @@ -550,16 +539,16 @@ final class BatteryController { // Represents whether the input device has a sysfs battery node. protected boolean mHasBattery = false; - protected final State mBluetoothState; @Nullable private BluetoothDevice mBluetoothDevice; + int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + long mBluetoothEventTime = 0; @Nullable private UEventBatteryListener mUEventBatteryListener; DeviceMonitor(int deviceId) { mState = new State(deviceId); - mBluetoothState = new State(deviceId); // Load the initial battery state and start monitoring. final long eventTime = SystemClock.uptimeMillis(); @@ -570,7 +559,7 @@ final class BatteryController { final State oldState = getBatteryStateForReporting(); changes.accept(eventTime); final State newState = getBatteryStateForReporting(); - if (!oldState.equals(newState)) { + if (!oldState.equalsIgnoringUpdateTime(newState)) { notifyAllListenersForDevice(newState); } } @@ -600,7 +589,14 @@ final class BatteryController { } mBluetoothDevice = bluetoothDevice; updateBluetoothMonitoring(); - updateBatteryStateFromBluetooth(eventTime); + mBluetoothEventTime = eventTime; + + if (mBluetoothDevice != null) { + mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( + mBluetoothDevice.getAddress()); + } else { + mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + } } } @@ -644,13 +640,6 @@ final class BatteryController { queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery)); } - protected void updateBatteryStateFromBluetooth(long eventTime) { - final State bluetoothState = mBluetoothDevice == null ? new State(mState.deviceId) - : queryBatteryStateFromBluetooth(mState.deviceId, eventTime, - mBluetoothDevice); - mBluetoothState.updateIfChanged(bluetoothState); - } - public void onPoll(long eventTime) { processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); } @@ -659,8 +648,11 @@ final class BatteryController { processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); } - public void onBluetoothBatteryChanged(long eventTime) { - processChangesAndNotify(eventTime, this::updateBatteryStateFromBluetooth); + public void onBluetoothBatteryChanged(long eventTime, int bluetoothBatteryLevel) { + processChangesAndNotify(eventTime, (time) -> { + mBluetoothBatteryLevel = bluetoothBatteryLevel; + mBluetoothEventTime = time; + }); } public boolean requiresPolling() { @@ -677,11 +669,18 @@ final class BatteryController { // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { - // Give precedence to the Bluetooth battery state if it's present. - if (mBluetoothState.isPresent) { - return new State(mBluetoothState); + // Give precedence to the Bluetooth battery state, and fall back to the native state. + return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), + () -> new State(mState)); + } + + @Nullable + protected State resolveBluetoothBatteryState() { + if (mBluetoothBatteryLevel < 0 || mBluetoothBatteryLevel > 100) { + return null; } - return new State(mState); + return new State(mState.deviceId, mBluetoothEventTime, true, + BatteryState.STATUS_UNKNOWN, mBluetoothBatteryLevel / 100.f); } @Override @@ -690,7 +689,7 @@ final class BatteryController { + ", Name='" + getInputDeviceName(mState.deviceId) + "'" + ", NativeBattery=" + mState + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none") - + ", BluetoothBattery=" + mBluetoothState; + + ", BluetoothState=" + resolveBluetoothBatteryState(); } } @@ -775,12 +774,10 @@ final class BatteryController { @Override public State getBatteryStateForReporting() { - // Give precedence to the Bluetooth battery state if it's present. - if (mBluetoothState.isPresent) { - return new State(mBluetoothState); - } - return mValidityTimeoutCallback != null - ? new State(mState) : new State(mState.deviceId); + // Give precedence to the Bluetooth battery state, and fall back to the native state. + return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), + () -> mValidityTimeoutCallback != null + ? new State(mState) : new State(mState.deviceId)); } @Override @@ -844,10 +841,10 @@ final class BatteryController { interface BluetoothBatteryManager { @VisibleForTesting interface BluetoothBatteryListener { - void onBluetoothBatteryChanged(long eventTime, String address); + void onBluetoothBatteryChanged(long eventTime, String address, int batteryLevel); } - void addListener(BluetoothBatteryListener listener); - void removeListener(BluetoothBatteryListener listener); + void addBatteryListener(BluetoothBatteryListener listener); + void removeBatteryListener(BluetoothBatteryListener listener); int getBatteryLevel(String address); } @@ -868,13 +865,13 @@ final class BatteryController { if (bluetoothDevice == null) { return; } - // We do not use the EXTRA_LEVEL value. Instead, the battery level will be queried - // from BluetoothDevice later so that we use a single source for the battery level. + final int batteryLevel = intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN); synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { final long eventTime = SystemClock.uptimeMillis(); mRegisteredListener.onBluetoothBatteryChanged( - eventTime, bluetoothDevice.getAddress()); + eventTime, bluetoothDevice.getAddress(), batteryLevel); } } } @@ -885,7 +882,7 @@ final class BatteryController { } @Override - public void addListener(BluetoothBatteryListener listener) { + public void addBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { throw new IllegalStateException( @@ -898,7 +895,7 @@ final class BatteryController { } @Override - public void removeListener(BluetoothBatteryListener listener) { + public void removeBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (!listener.equals(mRegisteredListener)) { throw new IllegalStateException("Listener is not registered."); @@ -954,7 +951,7 @@ final class BatteryController { this.capacity = capacity; } - private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { + public boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { long updateTime = this.updateTime; this.updateTime = other.updateTime; boolean eq = this.equals(other); 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 ecd9d893330ab..38847f284f505 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -657,28 +657,28 @@ class BatteryControllerTests { testLooper.dispatchNext() // Ensure that a BT battery listener is not added when there are no monitored BT devices. - verify(bluetoothBatteryManager, never()).addListener(any()) + verify(bluetoothBatteryManager, never()).addBatteryListener(any()) val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) val listener = createMockListener() // The BT battery listener is added when the first BT input device is monitored. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) // The BT listener is only added once for all BT devices. batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager, times(1)).addListener(any()) + verify(bluetoothBatteryManager, times(1)).addBatteryListener(any()) // The BT listener is only removed when there are no monitored BT devices. batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager, never()).removeListener(any()) + verify(bluetoothBatteryManager, never()).removeBatteryListener(any()) `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn(null) notifyDeviceChanged(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - verify(bluetoothBatteryManager).removeListener(bluetoothListener.value) + verify(bluetoothBatteryManager).removeBatteryListener(bluetoothListener.value) } @Test @@ -690,15 +690,14 @@ class BatteryControllerTests { val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) val listener = createMockListener() batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // When the state has not changed, the listener is not notified again. - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 21) listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(25) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 25) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f) } @@ -717,7 +716,7 @@ class BatteryControllerTests { // When the device is first monitored and both native and BT battery is available, // the latter is used. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), @@ -744,25 +743,22 @@ class BatteryControllerTests { val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // Fall back to the native state when BT is off. - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))) - .thenReturn(BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.98f) - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(22) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 22) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) // Fall back to the native state when BT battery is unknown. - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))) - .thenReturn(BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_UNKNOWN) listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) } } From 1f64ce023c2e88cfec5660f5c036d5d6e053fd5f Mon Sep 17 00:00:00 2001 From: Prabir Pradhan Date: Wed, 4 Jan 2023 02:54:31 +0000 Subject: [PATCH 2/2] BatteryController: Listen to battery changes from Bluetooth metadata Some Bluetooth devices report battery state through Bluetooth metadata, which can be obtained through BluetoothDevice#getMetadata. This is another channel through which battery information can be obtained, and is different than the BluetoothDevice#getBatteryLevel API which reports the values from the Bluetooth Hands-Free Protocol (HFP). In this CL, we start listening to Bluetooth metadata changes when a Bluetooth input device is added. In particular, we parse the following two keys: - METADATA_MAIN_BATTERY - the battery level of the device - METADATA_MAIN_CHARGING - whether or not the battery is being charged To start listening to changes in metadata values, we add an OnMetadataChangedListener for each monitored Bluetooth input device. We prioritize the battery level from the metadata over the battery level obtained from the BluetoothDevice#getBatteryLevel API. Bug: 243005009 Test: atest BatteryControllerTests Change-Id: I28d77e660b18821b14a67798fb88a5e926a7bebc --- .../server/input/BatteryController.java | 160 ++++++++++++++++-- .../server/input/BatteryControllerTests.kt | 135 ++++++++++++++- 2 files changed, 278 insertions(+), 17 deletions(-) diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index b0c99c05c5831..993b4fdb2498f 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -31,6 +31,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; @@ -51,6 +52,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -102,7 +104,7 @@ final class BatteryController { BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) { this(context, nativeService, looper, new UEventManager() {}, - new LocalBluetoothBatteryManager(context)); + new LocalBluetoothBatteryManager(context, looper)); } @VisibleForTesting @@ -163,7 +165,7 @@ final class BatteryController { // This is the first listener that is monitoring this device. monitor = new DeviceMonitor(deviceId); mDeviceMonitors.put(deviceId, monitor); - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } if (DEBUG) { @@ -390,6 +392,18 @@ final class BatteryController { } } + private void handleBluetoothMetadataChange(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + synchronized (mLock) { + final DeviceMonitor monitor = + findIf(mDeviceMonitors, (m) -> device.equals(m.mBluetoothDevice)); + if (monitor != null) { + final long eventTime = SystemClock.uptimeMillis(); + monitor.onBluetoothMetadataChanged(eventTime, key, value); + } + } + } + /** Gets the current battery state of an input device. */ public IInputDeviceBatteryState getBatteryState(int deviceId) { synchronized (mLock) { @@ -515,7 +529,7 @@ final class BatteryController { isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); } - private void updateBluetoothMonitoring() { + private void updateBluetoothBatteryMonitoring() { synchronized (mLock) { if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { // At least one input device being monitored is connected over Bluetooth. @@ -541,8 +555,15 @@ final class BatteryController { @Nullable private BluetoothDevice mBluetoothDevice; - int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; long mBluetoothEventTime = 0; + // The battery level reported by the Bluetooth Hands-Free Profile (HPF) obtained through + // BluetoothDevice#getBatteryLevel(). + int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + // The battery level and status reported through the Bluetooth device's metadata. + int mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + int mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + @Nullable + private BluetoothAdapter.OnMetadataChangedListener mBluetoothMetadataListener; @Nullable private UEventBatteryListener mUEventBatteryListener; @@ -583,19 +604,21 @@ final class BatteryController { final BluetoothDevice bluetoothDevice = getBluetoothDevice(deviceId); if (!Objects.equals(mBluetoothDevice, bluetoothDevice)) { if (DEBUG) { - Slog.d(TAG, "Bluetooth device " - + ((bluetoothDevice != null) ? "is" : "is not") - + " now present for deviceId " + deviceId); + Slog.d(TAG, "Bluetooth device is now " + + ((bluetoothDevice != null) ? "" : "not") + + " present for deviceId " + deviceId); } + + mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + stopBluetoothMetadataMonitoring(); + mBluetoothDevice = bluetoothDevice; - updateBluetoothMonitoring(); - mBluetoothEventTime = eventTime; + updateBluetoothBatteryMonitoring(); if (mBluetoothDevice != null) { mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( mBluetoothDevice.getAddress()); - } else { - mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + startBluetoothMetadataMonitoring(eventTime); } } } @@ -628,11 +651,39 @@ final class BatteryController { } } + private void startBluetoothMetadataMonitoring(long eventTime) { + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothMetadataListener = BatteryController.this::handleBluetoothMetadataChange; + mBluetoothBatteryManager.addMetadataListener(mBluetoothDevice.getAddress(), + mBluetoothMetadataListener); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_BATTERY, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_BATTERY)); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_CHARGING, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_CHARGING)); + } + + private void stopBluetoothMetadataMonitoring() { + if (mBluetoothMetadataListener == null) { + return; + } + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothBatteryManager.removeMetadataListener( + mBluetoothDevice.getAddress(), mBluetoothMetadataListener); + mBluetoothMetadataListener = null; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + // This must be called when the device is no longer being monitored. public void onMonitorDestroy() { stopNativeMonitoring(); + stopBluetoothMetadataMonitoring(); mBluetoothDevice = null; - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } protected void updateBatteryStateFromNative(long eventTime) { @@ -655,6 +706,46 @@ final class BatteryController { }); } + public void onBluetoothMetadataChanged(long eventTime, int key, @Nullable byte[] value) { + processChangesAndNotify(eventTime, + (time) -> updateBluetoothMetadataState(time, key, value)); + } + + private void updateBluetoothMetadataState(long eventTime, int key, + @Nullable byte[] value) { + switch (key) { + case BluetoothDevice.METADATA_MAIN_BATTERY: + mBluetoothEventTime = eventTime; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + if (value != null) { + try { + mBluetoothMetadataBatteryLevel = Integer.parseInt( + new String(value)); + } catch (NumberFormatException e) { + Slog.wtf(TAG, + "Failed to parse bluetooth METADATA_MAIN_BATTERY with " + + "value '" + + new String(value) + "' for device " + + mBluetoothDevice); + } + } + break; + case BluetoothDevice.METADATA_MAIN_CHARGING: + mBluetoothEventTime = eventTime; + if (value != null) { + mBluetoothMetadataBatteryStatus = Boolean.parseBoolean( + new String(value)) + ? BatteryState.STATUS_CHARGING + : BatteryState.STATUS_DISCHARGING; + } else { + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + break; + default: + break; + } + } + public boolean requiresPolling() { return true; } @@ -676,11 +767,18 @@ final class BatteryController { @Nullable protected State resolveBluetoothBatteryState() { - if (mBluetoothBatteryLevel < 0 || mBluetoothBatteryLevel > 100) { + final int level; + // Prefer battery level obtained from the metadata over the Bluetooth Hands-Free + // Profile (HFP). + if (mBluetoothMetadataBatteryLevel >= 0 && mBluetoothMetadataBatteryLevel <= 100) { + level = mBluetoothMetadataBatteryLevel; + } else if (mBluetoothBatteryLevel >= 0 && mBluetoothBatteryLevel <= 100) { + level = mBluetoothBatteryLevel; + } else { return null; } return new State(mState.deviceId, mBluetoothEventTime, true, - BatteryState.STATUS_UNKNOWN, mBluetoothBatteryLevel / 100.f); + mBluetoothMetadataBatteryStatus, level / 100.f); } @Override @@ -843,13 +941,22 @@ final class BatteryController { interface BluetoothBatteryListener { void onBluetoothBatteryChanged(long eventTime, String address, int batteryLevel); } + // Methods used for obtaining the Bluetooth battery level through Bluetooth HFP. void addBatteryListener(BluetoothBatteryListener listener); void removeBatteryListener(BluetoothBatteryListener listener); int getBatteryLevel(String address); + + // Methods used for obtaining the battery level through Bluetooth metadata. + void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + byte[] getMetadata(String address, int key); } private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager { private final Context mContext; + private final Executor mExecutor; @Nullable @GuardedBy("mBroadcastReceiver") private BluetoothBatteryListener mRegisteredListener; @@ -877,8 +984,9 @@ final class BatteryController { } }; - LocalBluetoothBatteryManager(Context context) { + LocalBluetoothBatteryManager(Context context, Looper looper) { mContext = context; + mExecutor = new HandlerExecutor(new Handler(looper)); } @Override @@ -909,6 +1017,28 @@ final class BatteryController { public int getBatteryLevel(String address) { return getBluetoothDevice(mContext, address).getBatteryLevel(); } + + @Override + public void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().addOnMetadataChangedListener( + getBluetoothDevice(mContext, address), mExecutor, + listener); + } + + @Override + public void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().removeOnMetadataChangedListener( + getBluetoothDevice(mContext, address), listener); + } + + @Override + public byte[] getMetadata(String address, int key) { + return getBluetoothDevice(mContext, address).getMetadata(key); + } } // Helper class that adds copying and printing functionality to IInputDeviceBatteryState. 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 38847f284f505..3ce747f145dc2 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -16,7 +16,9 @@ package com.android.server.input +import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager import android.content.Context import android.content.ContextWrapper import android.hardware.BatteryState.STATUS_CHARGING @@ -246,6 +248,11 @@ class BatteryControllerTests { notifyDeviceChanged(deviceId, hasBattery, supportsUsi) } + private fun createBluetoothDevice(address: String): BluetoothDevice { + return context.getSystemService(BluetoothManager::class.java)!! + .adapter.getRemoteDevice(address) + } + @After fun tearDown() { InputManager.clearInstance() @@ -656,11 +663,13 @@ class BatteryControllerTests { addInputDevice(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - // Ensure that a BT battery listener is not added when there are no monitored BT devices. + // Listen to a non-Bluetooth device and ensure that the BT battery listener is not added + // when there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) verify(bluetoothBatteryManager, never()).addBatteryListener(any()) val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) - val listener = createMockListener() // The BT battery listener is added when the first BT input device is monitored. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) @@ -761,4 +770,126 @@ class BatteryControllerTests { BluetoothDevice.BATTERY_LEVEL_UNKNOWN) listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) } + + @Test + fun testRegisterBluetoothMetadataListenerForMonitoredBluetoothDevices() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn("11:22:33:44:55:66") + addInputDevice(BT_DEVICE_ID) + testLooper.dispatchNext() + addInputDevice(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + + // Listen to a non-Bluetooth device and ensure that the metadata listener is not added when + // there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addMetadataListener(any(), any()) + + val metadataListener1 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener2 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + + // The metadata listener is added when the first BT input device is monitored. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener1.capture()) + + // There is one metadata listener added for each BT device. + batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("11:22:33:44:55:66"), metadataListener2.capture()) + + // The metadata listener is removed when the device is no longer monitored. + batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .removeMetadataListener("AA:BB:CC:DD:EE:FF", metadataListener1.value) + + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn(null) + notifyDeviceChanged(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + verify(bluetoothBatteryManager) + .removeMetadataListener("11:22:33:44:55:66", metadataListener2.value) + } + + @Test + fun testNotifiesBluetoothMetadataBatteryChanges() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("21".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f, status = STATUS_UNKNOWN) + + // When the state has not changed, the listener is not notified again. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "21".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "25".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_UNKNOWN) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "true".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_CHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "false".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_DISCHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, null) + listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.25f, + status = STATUS_UNKNOWN) + } + + @Test + fun testBluetoothMetadataBatteryIsPrioritized() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("22".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) + + // A change in the Bluetooth battery level has no effect while there is a valid battery + // level obtained through the metadata. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 23) + listener.verifyNotified(BT_DEVICE_ID, mode = never(), capacity = 0.23f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "24".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.24f) + + // When the battery level from the metadata is no longer valid, we fall back to using the + // Bluetooth battery level. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, null) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.23f) + } }