diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index c99a7a0de79cb..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) { @@ -378,13 +380,26 @@ 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); + } + } + } + + 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); } } } @@ -514,31 +529,19 @@ 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() { + private void updateBluetoothBatteryMonitoring() { synchronized (mLock) { if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { // At least one input device being monitored is connected over Bluetooth. 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 +553,23 @@ 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; + 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; 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 +580,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); } } @@ -594,13 +604,22 @@ 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(); - updateBatteryStateFromBluetooth(eventTime); + updateBluetoothBatteryMonitoring(); + + if (mBluetoothDevice != null) { + mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( + mBluetoothDevice.getAddress()); + startBluetoothMetadataMonitoring(eventTime); + } } } @@ -632,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) { @@ -644,13 +691,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 +699,51 @@ 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 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() { @@ -677,11 +760,25 @@ 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() { + 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); + return new State(mState.deviceId, mBluetoothEventTime, true, + mBluetoothMetadataBatteryStatus, level / 100.f); } @Override @@ -690,7 +787,7 @@ final class BatteryController { + ", Name='" + getInputDeviceName(mState.deviceId) + "'" + ", NativeBattery=" + mState + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none") - + ", BluetoothBattery=" + mBluetoothState; + + ", BluetoothState=" + resolveBluetoothBatteryState(); } } @@ -775,12 +872,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,15 +939,24 @@ 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); + // 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; @@ -868,24 +972,25 @@ 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); } } } }; - LocalBluetoothBatteryManager(Context context) { + LocalBluetoothBatteryManager(Context context, Looper looper) { mContext = context; + mExecutor = new HandlerExecutor(new Handler(looper)); } @Override - public void addListener(BluetoothBatteryListener listener) { + public void addBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { throw new IllegalStateException( @@ -898,7 +1003,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."); @@ -912,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. @@ -954,7 +1081,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..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,29 +663,31 @@ 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. - verify(bluetoothBatteryManager, never()).addListener(any()) + // 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) - 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 +699,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 +725,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 +752,144 @@ 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) } + + @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) + } }