BatteryController: Add battery monitoring from Bluetooth

For input devices that are connected over Bluetooth, we will monitor
their Bluetooth battery state by registering a BroadcastReceiver to
listen to battery changes over Bluetooth.

We always prioritize reporting the Bluetooth battery state first, and if
invalid, fall back to using the battery state queried through the sysfs
node.

In this CL, we add a BluetoothBatteryManager interface to hide the
details of dealing with the Bluetooth APIs to simplify testing.

Bug: 243005009
Test: atest FrameworkServicesTest
Test: manual, with Lenovo Precision Pen 3
Change-Id: I40864c0aeaf72c252a5f8d0b2217903613ca9543
This commit is contained in:
Prabir Pradhan
2022-10-12 23:05:17 +00:00
parent bc1cd9c1f5
commit 854d4bdee7
3 changed files with 352 additions and 24 deletions

View File

@@ -19,7 +19,13 @@ package com.android.server.input;
import android.annotation.BinderThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.BatteryState;
import android.hardware.input.IInputDeviceBatteryListener;
import android.hardware.input.IInputDeviceBatteryState;
@@ -46,6 +52,7 @@ import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
/**
@@ -74,6 +81,7 @@ final class BatteryController {
private final NativeInputManagerService mNative;
private final Handler mHandler;
private final UEventManager mUEventManager;
private final BluetoothBatteryManager mBluetoothBatteryManager;
// Maps a pid to the registered listener record for that process. There can only be one battery
// listener per process.
@@ -88,18 +96,23 @@ final class BatteryController {
private boolean mIsPolling = false;
@GuardedBy("mLock")
private boolean mIsInteractive = true;
@Nullable
@GuardedBy("mLock")
private BluetoothBatteryManager.BluetoothBatteryListener mBluetoothBatteryListener;
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) {
this(context, nativeService, looper, new UEventManager() {});
this(context, nativeService, looper, new UEventManager() {},
new LocalBluetoothBatteryManager(context));
}
@VisibleForTesting
BatteryController(Context context, NativeInputManagerService nativeService, Looper looper,
UEventManager uEventManager) {
UEventManager uEventManager, BluetoothBatteryManager bbm) {
mContext = context;
mNative = nativeService;
mHandler = new Handler(looper);
mUEventManager = uEventManager;
mBluetoothBatteryManager = bbm;
}
public void systemRunning() {
@@ -150,6 +163,7 @@ final class BatteryController {
// This is the first listener that is monitoring this device.
monitor = new DeviceMonitor(deviceId);
mDeviceMonitors.put(deviceId, monitor);
updateBluetoothMonitoring();
}
if (DEBUG) {
@@ -202,25 +216,39 @@ final class BatteryController {
mHandler.postDelayed(this::handlePollEvent, delayStart ? POLLING_PERIOD_MILLIS : 0);
}
private String getInputDeviceName(int deviceId) {
private <R> R processInputDevice(int deviceId, R defaultValue, Function<InputDevice, R> func) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
.getInputDevice(deviceId);
return device != null ? device.getName() : "<none>";
return device == null ? defaultValue : func.apply(device);
}
private String getInputDeviceName(int deviceId) {
return processInputDevice(deviceId, "<none>" /*defaultValue*/, InputDevice::getName);
}
private boolean hasBattery(int deviceId) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
.getInputDevice(deviceId);
return device != null && device.hasBattery();
return processInputDevice(deviceId, false /*defaultValue*/, InputDevice::hasBattery);
}
private boolean isUsiDevice(int deviceId) {
final InputDevice device =
Objects.requireNonNull(mContext.getSystemService(InputManager.class))
.getInputDevice(deviceId);
return device != null && device.supportsUsi();
return processInputDevice(deviceId, false /*defaultValue*/, InputDevice::supportsUsi);
}
@Nullable
private BluetoothDevice getBluetoothDevice(int inputDeviceId) {
return getBluetoothDevice(mContext,
processInputDevice(inputDeviceId, null /*defaultValue*/,
InputDevice::getBluetoothAddress));
}
@Nullable
private static BluetoothDevice getBluetoothDevice(Context context, String address) {
if (address == null) return null;
final BluetoothAdapter adapter =
Objects.requireNonNull(context.getSystemService(BluetoothManager.class))
.getAdapter();
return adapter.getRemoteDevice(address);
}
@GuardedBy("mLock")
@@ -350,6 +378,17 @@ final class BatteryController {
}
}
private void handleBluetoothBatteryLevelChange(long eventTime, String address) {
synchronized (mLock) {
final DeviceMonitor monitor = findIf(mDeviceMonitors, (m) ->
(m.mBluetoothDevice != null
&& address.equals(m.mBluetoothDevice.getAddress())));
if (monitor != null) {
monitor.onBluetoothBatteryChanged(eventTime);
}
}
}
/** Gets the current battery state of an input device. */
public IInputDeviceBatteryState getBatteryState(int deviceId) {
synchronized (mLock) {
@@ -475,17 +514,52 @@ 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)) {
// 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);
}
} 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);
mBluetoothBatteryListener = null;
}
}
}
// Holds the state of an InputDevice for which battery changes are currently being monitored.
private class DeviceMonitor {
protected final State mState;
// Represents whether the input device has a sysfs battery node.
protected boolean mHasBattery = false;
protected final State mBluetoothState;
@Nullable
private BluetoothDevice mBluetoothDevice;
@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();
@@ -506,18 +580,31 @@ final class BatteryController {
}
private void configureDeviceMonitor(long eventTime) {
final int deviceId = mState.deviceId;
if (mHasBattery != hasBattery(mState.deviceId)) {
mHasBattery = !mHasBattery;
if (mHasBattery) {
startMonitoring();
startNativeMonitoring();
} else {
stopMonitoring();
stopNativeMonitoring();
}
updateBatteryStateFromNative(eventTime);
}
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);
}
mBluetoothDevice = bluetoothDevice;
updateBluetoothMonitoring();
updateBatteryStateFromBluetooth(eventTime);
}
}
private void startMonitoring() {
private void startNativeMonitoring() {
final String batteryPath = mNative.getBatteryDevicePath(mState.deviceId);
if (batteryPath == null) {
return;
@@ -538,7 +625,7 @@ final class BatteryController {
return path.startsWith("/sys") ? path.substring(4) : path;
}
private void stopMonitoring() {
private void stopNativeMonitoring() {
if (mUEventBatteryListener != null) {
mUEventManager.removeListener(mUEventBatteryListener);
mUEventBatteryListener = null;
@@ -547,7 +634,9 @@ final class BatteryController {
// This must be called when the device is no longer being monitored.
public void onMonitorDestroy() {
stopMonitoring();
stopNativeMonitoring();
mBluetoothDevice = null;
updateBluetoothMonitoring();
}
protected void updateBatteryStateFromNative(long eventTime) {
@@ -555,6 +644,13 @@ 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);
}
@@ -563,6 +659,10 @@ final class BatteryController {
processChangesAndNotify(eventTime, this::updateBatteryStateFromNative);
}
public void onBluetoothBatteryChanged(long eventTime) {
processChangesAndNotify(eventTime, this::updateBatteryStateFromBluetooth);
}
public boolean requiresPolling() {
return true;
}
@@ -577,6 +677,10 @@ 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);
}
return new State(mState);
}
@@ -585,7 +689,8 @@ final class BatteryController {
return "DeviceId=" + mState.deviceId
+ ", Name='" + getInputDeviceName(mState.deviceId) + "'"
+ ", NativeBattery=" + mState
+ ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none");
+ ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none")
+ ", BluetoothBattery=" + mBluetoothState;
}
}
@@ -670,6 +775,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);
}
@@ -729,6 +838,82 @@ final class BatteryController {
}
}
// An interface used to change the API of adding a bluetooth battery listener to a more
// test-friendly format.
@VisibleForTesting
interface BluetoothBatteryManager {
@VisibleForTesting
interface BluetoothBatteryListener {
void onBluetoothBatteryChanged(long eventTime, String address);
}
void addListener(BluetoothBatteryListener listener);
void removeListener(BluetoothBatteryListener listener);
int getBatteryLevel(String address);
}
private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager {
private final Context mContext;
@Nullable
@GuardedBy("mBroadcastReceiver")
private BluetoothBatteryListener mRegisteredListener;
@GuardedBy("mBroadcastReceiver")
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED.equals(intent.getAction())) {
return;
}
final BluetoothDevice bluetoothDevice = intent.getParcelableExtra(
BluetoothDevice.EXTRA_DEVICE, BluetoothDevice.class);
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.
synchronized (mBroadcastReceiver) {
if (mRegisteredListener != null) {
final long eventTime = SystemClock.uptimeMillis();
mRegisteredListener.onBluetoothBatteryChanged(
eventTime, bluetoothDevice.getAddress());
}
}
}
};
LocalBluetoothBatteryManager(Context context) {
mContext = context;
}
@Override
public void addListener(BluetoothBatteryListener listener) {
synchronized (mBroadcastReceiver) {
if (mRegisteredListener != null) {
throw new IllegalStateException(
"Only one bluetooth battery listener can be registered at once.");
}
mRegisteredListener = listener;
mContext.registerReceiver(mBroadcastReceiver,
new IntentFilter(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED));
}
}
@Override
public void removeListener(BluetoothBatteryListener listener) {
synchronized (mBroadcastReceiver) {
if (!listener.equals(mRegisteredListener)) {
throw new IllegalStateException("Listener is not registered.");
}
mRegisteredListener = null;
mContext.unregisterReceiver(mBroadcastReceiver);
}
}
@Override
public int getBatteryLevel(String address) {
return getBluetoothDevice(mContext, address).getBatteryLevel();
}
}
// Helper class that adds copying and printing functionality to IInputDeviceBatteryState.
private static class State extends IInputDeviceBatteryState {
@@ -792,11 +977,17 @@ final class BatteryController {
// Check if any value in an ArrayMap matches the predicate in an optimized way.
private static <K, V> boolean anyOf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
return findIf(arrayMap, test) != null;
}
// Find the first value in an ArrayMap that matches the predicate in an optimized way.
private static <K, V> V findIf(ArrayMap<K, V> arrayMap, Predicate<V> test) {
for (int i = 0; i < arrayMap.size(); i++) {
if (test.test(arrayMap.valueAt(i))) {
return true;
final V value = arrayMap.valueAt(i);
if (test.test(value)) {
return value;
}
}
return false;
return null;
}
}

View File

@@ -2682,7 +2682,13 @@ public class InputManagerService extends IInputManager.Stub
public String getInputDeviceBluetoothAddress(int deviceId) {
super.getInputDeviceBluetoothAddress_enforcePermission();
return mNative.getBluetoothAddress(deviceId);
final String address = mNative.getBluetoothAddress(deviceId);
if (address == null) return null;
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
throw new IllegalStateException("The Bluetooth address of input device " + deviceId
+ " should not be invalid: address=" + address);
}
return address;
}
@EnforcePermission(Manifest.permission.MONITOR_INPUT)

View File

@@ -16,6 +16,7 @@
package com.android.server.input
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.content.ContextWrapper
import android.hardware.BatteryState.STATUS_CHARGING
@@ -33,6 +34,8 @@ import android.os.test.TestLooper
import android.platform.test.annotations.Presubmit
import android.view.InputDevice
import androidx.test.InstrumentationRegistry
import com.android.server.input.BatteryController.BluetoothBatteryManager
import com.android.server.input.BatteryController.BluetoothBatteryManager.BluetoothBatteryListener
import com.android.server.input.BatteryController.POLLING_PERIOD_MILLIS
import com.android.server.input.BatteryController.UEventManager
import com.android.server.input.BatteryController.UEventManager.UEventBatteryListener
@@ -52,6 +55,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.notNull
import org.mockito.Mock
import org.mockito.Mockito.anyInt
@@ -172,6 +176,8 @@ class BatteryControllerTests {
const val SECOND_DEVICE_ID = 11
const val USI_DEVICE_ID = 101
const val SECOND_USI_DEVICE_ID = 102
const val BT_DEVICE_ID = 100001
const val SECOND_BT_DEVICE_ID = 100002
const val TIMESTAMP = 123456789L
}
@@ -184,6 +190,8 @@ class BatteryControllerTests {
private lateinit var iInputManager: IInputManager
@Mock
private lateinit var uEventManager: UEventManager
@Mock
private lateinit var bluetoothBatteryManager: BluetoothBatteryManager
private lateinit var batteryController: BatteryController
private lateinit var context: Context
@@ -203,11 +211,13 @@ class BatteryControllerTests {
addInputDevice(DEVICE_ID)
addInputDevice(SECOND_DEVICE_ID)
batteryController = BatteryController(context, native, testLooper.looper, uEventManager)
batteryController = BatteryController(context, native, testLooper.looper, uEventManager,
bluetoothBatteryManager)
batteryController.systemRunning()
val listenerCaptor = ArgumentCaptor.forClass(IInputDevicesChangedListener::class.java)
verify(iInputManager).registerInputDevicesChangedListener(listenerCaptor.capture())
devicesChangedListener = listenerCaptor.value
testLooper.dispatchAll()
}
private fun notifyDeviceChanged(
@@ -230,7 +240,7 @@ class BatteryControllerTests {
private fun addInputDevice(
deviceId: Int,
hasBattery: Boolean = true,
supportsUsi: Boolean = false
supportsUsi: Boolean = false,
) {
deviceGenerationMap[deviceId] = 0
notifyDeviceChanged(deviceId, hasBattery, supportsUsi)
@@ -634,4 +644,125 @@ class BatteryControllerTests {
assertThat("battery state matches", batteryController.getBatteryState(USI_DEVICE_ID),
matchesState(USI_DEVICE_ID, status = STATUS_UNKNOWN, capacity = 0f))
}
@Test
fun testRegisterBluetoothListenerForMonitoredBluetoothDevices() {
`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()
// Ensure that a BT battery listener is not added when there are no monitored BT devices.
verify(bluetoothBatteryManager, never()).addListener(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())
// 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())
// 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())
`when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID))
.thenReturn(null)
notifyDeviceChanged(SECOND_BT_DEVICE_ID)
testLooper.dispatchNext()
verify(bluetoothBatteryManager).removeListener(bluetoothListener.value)
}
@Test
fun testNotifiesBluetoothBatteryChanges() {
`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)
addInputDevice(BT_DEVICE_ID)
val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
val listener = createMockListener()
batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID)
verify(bluetoothBatteryManager).addListener(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")
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")
listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f)
}
@Test
fun testBluetoothBatteryIsPrioritized() {
`when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device")
`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`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(98)
addInputDevice(BT_DEVICE_ID)
val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
val listener = createMockListener()
val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
// 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(uEventManager).addListener(uEventListener.capture(), any())
listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f)
assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID),
matchesState(BT_DEVICE_ID, capacity = 0.21f))
// If only the native battery state changes the listener is not notified.
`when`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(97)
uEventListener.value!!.onBatteryUEvent(TIMESTAMP)
listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f)
assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID),
matchesState(BT_DEVICE_ID, capacity = 0.21f))
}
@Test
fun testFallBackToNativeBatteryStateWhenBluetoothStateInvalid() {
`when`(native.getBatteryDevicePath(BT_DEVICE_ID)).thenReturn("/sys/dev/bt_device")
`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`(native.getBatteryCapacity(BT_DEVICE_ID)).thenReturn(98)
addInputDevice(BT_DEVICE_ID)
val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java)
val listener = createMockListener()
val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java)
batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID)
verify(bluetoothBatteryManager).addListener(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")
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())
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")
listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f)
}
}