summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/input/BatteryController.java233
-rw-r--r--services/core/java/com/android/server/input/InputManagerService.java8
-rw-r--r--services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt135
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)
+ }
}