From 854d4bdee790bedbf504cbdfc41fd601dee28e03 Mon Sep 17 00:00:00 2001 From: Prabir Pradhan Date: Wed, 12 Oct 2022 23:05:17 +0000 Subject: 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 --- .../android/server/input/BatteryController.java | 233 +++++++++++++++++++-- .../android/server/input/InputManagerService.java | 8 +- .../android/server/input/BatteryControllerTests.kt | 135 +++++++++++- 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 processInputDevice(int deviceId, R defaultValue, Function func) { final InputDevice device = Objects.requireNonNull(mContext.getSystemService(InputManager.class)) .getInputDevice(deviceId); - return device != null ? device.getName() : ""; + return device == null ? defaultValue : func.apply(device); + } + + private String getInputDeviceName(int deviceId) { + return processInputDevice(deviceId, "" /*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 boolean anyOf(ArrayMap arrayMap, Predicate test) { + return findIf(arrayMap, test) != null; + } + + // Find the first value in an ArrayMap that matches the predicate in an optimized way. + private static V findIf(ArrayMap arrayMap, Predicate 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 8497dfb58ba6..24586c2e15d3 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -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) 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) + } } -- cgit v1.2.3-59-g8ed1b