diff options
3 files changed, 352 insertions, 24 deletions
diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index c83fa2d3942c..c99a7a0de79c 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -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; } } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 1274f026297e..199519c19785 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2126,7 +2126,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) 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 6590a2b500e4..ecd9d893330a 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -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) + } } |