diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index b0c99c05c5831..993b4fdb2498f 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -31,6 +31,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; @@ -51,6 +52,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -102,7 +104,7 @@ final class BatteryController { BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) { this(context, nativeService, looper, new UEventManager() {}, - new LocalBluetoothBatteryManager(context)); + new LocalBluetoothBatteryManager(context, looper)); } @VisibleForTesting @@ -163,7 +165,7 @@ final class BatteryController { // This is the first listener that is monitoring this device. monitor = new DeviceMonitor(deviceId); mDeviceMonitors.put(deviceId, monitor); - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } if (DEBUG) { @@ -390,6 +392,18 @@ final class BatteryController { } } + private void handleBluetoothMetadataChange(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + synchronized (mLock) { + final DeviceMonitor monitor = + findIf(mDeviceMonitors, (m) -> device.equals(m.mBluetoothDevice)); + if (monitor != null) { + final long eventTime = SystemClock.uptimeMillis(); + monitor.onBluetoothMetadataChanged(eventTime, key, value); + } + } + } + /** Gets the current battery state of an input device. */ public IInputDeviceBatteryState getBatteryState(int deviceId) { synchronized (mLock) { @@ -515,7 +529,7 @@ final class BatteryController { isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); } - private void updateBluetoothMonitoring() { + private void updateBluetoothBatteryMonitoring() { synchronized (mLock) { if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { // At least one input device being monitored is connected over Bluetooth. @@ -541,8 +555,15 @@ final class BatteryController { @Nullable private BluetoothDevice mBluetoothDevice; - int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; long mBluetoothEventTime = 0; + // The battery level reported by the Bluetooth Hands-Free Profile (HPF) obtained through + // BluetoothDevice#getBatteryLevel(). + int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + // The battery level and status reported through the Bluetooth device's metadata. + int mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + int mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + @Nullable + private BluetoothAdapter.OnMetadataChangedListener mBluetoothMetadataListener; @Nullable private UEventBatteryListener mUEventBatteryListener; @@ -583,19 +604,21 @@ final class BatteryController { final BluetoothDevice bluetoothDevice = getBluetoothDevice(deviceId); if (!Objects.equals(mBluetoothDevice, bluetoothDevice)) { if (DEBUG) { - Slog.d(TAG, "Bluetooth device " - + ((bluetoothDevice != null) ? "is" : "is not") - + " now present for deviceId " + deviceId); + Slog.d(TAG, "Bluetooth device is now " + + ((bluetoothDevice != null) ? "" : "not") + + " present for deviceId " + deviceId); } + + mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + stopBluetoothMetadataMonitoring(); + mBluetoothDevice = bluetoothDevice; - updateBluetoothMonitoring(); - mBluetoothEventTime = eventTime; + updateBluetoothBatteryMonitoring(); if (mBluetoothDevice != null) { mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( mBluetoothDevice.getAddress()); - } else { - mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + startBluetoothMetadataMonitoring(eventTime); } } } @@ -628,11 +651,39 @@ final class BatteryController { } } + private void startBluetoothMetadataMonitoring(long eventTime) { + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothMetadataListener = BatteryController.this::handleBluetoothMetadataChange; + mBluetoothBatteryManager.addMetadataListener(mBluetoothDevice.getAddress(), + mBluetoothMetadataListener); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_BATTERY, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_BATTERY)); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_CHARGING, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_CHARGING)); + } + + private void stopBluetoothMetadataMonitoring() { + if (mBluetoothMetadataListener == null) { + return; + } + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothBatteryManager.removeMetadataListener( + mBluetoothDevice.getAddress(), mBluetoothMetadataListener); + mBluetoothMetadataListener = null; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + // This must be called when the device is no longer being monitored. public void onMonitorDestroy() { stopNativeMonitoring(); + stopBluetoothMetadataMonitoring(); mBluetoothDevice = null; - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } protected void updateBatteryStateFromNative(long eventTime) { @@ -655,6 +706,46 @@ final class BatteryController { }); } + public void onBluetoothMetadataChanged(long eventTime, int key, @Nullable byte[] value) { + processChangesAndNotify(eventTime, + (time) -> updateBluetoothMetadataState(time, key, value)); + } + + private void updateBluetoothMetadataState(long eventTime, int key, + @Nullable byte[] value) { + switch (key) { + case BluetoothDevice.METADATA_MAIN_BATTERY: + mBluetoothEventTime = eventTime; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + if (value != null) { + try { + mBluetoothMetadataBatteryLevel = Integer.parseInt( + new String(value)); + } catch (NumberFormatException e) { + Slog.wtf(TAG, + "Failed to parse bluetooth METADATA_MAIN_BATTERY with " + + "value '" + + new String(value) + "' for device " + + mBluetoothDevice); + } + } + break; + case BluetoothDevice.METADATA_MAIN_CHARGING: + mBluetoothEventTime = eventTime; + if (value != null) { + mBluetoothMetadataBatteryStatus = Boolean.parseBoolean( + new String(value)) + ? BatteryState.STATUS_CHARGING + : BatteryState.STATUS_DISCHARGING; + } else { + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + break; + default: + break; + } + } + public boolean requiresPolling() { return true; } @@ -676,11 +767,18 @@ final class BatteryController { @Nullable protected State resolveBluetoothBatteryState() { - if (mBluetoothBatteryLevel < 0 || mBluetoothBatteryLevel > 100) { + final int level; + // Prefer battery level obtained from the metadata over the Bluetooth Hands-Free + // Profile (HFP). + if (mBluetoothMetadataBatteryLevel >= 0 && mBluetoothMetadataBatteryLevel <= 100) { + level = mBluetoothMetadataBatteryLevel; + } else if (mBluetoothBatteryLevel >= 0 && mBluetoothBatteryLevel <= 100) { + level = mBluetoothBatteryLevel; + } else { return null; } return new State(mState.deviceId, mBluetoothEventTime, true, - BatteryState.STATUS_UNKNOWN, mBluetoothBatteryLevel / 100.f); + mBluetoothMetadataBatteryStatus, level / 100.f); } @Override @@ -843,13 +941,22 @@ final class BatteryController { interface BluetoothBatteryListener { void onBluetoothBatteryChanged(long eventTime, String address, int batteryLevel); } + // Methods used for obtaining the Bluetooth battery level through Bluetooth HFP. void addBatteryListener(BluetoothBatteryListener listener); void removeBatteryListener(BluetoothBatteryListener listener); int getBatteryLevel(String address); + + // Methods used for obtaining the battery level through Bluetooth metadata. + void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + byte[] getMetadata(String address, int key); } private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager { private final Context mContext; + private final Executor mExecutor; @Nullable @GuardedBy("mBroadcastReceiver") private BluetoothBatteryListener mRegisteredListener; @@ -877,8 +984,9 @@ final class BatteryController { } }; - LocalBluetoothBatteryManager(Context context) { + LocalBluetoothBatteryManager(Context context, Looper looper) { mContext = context; + mExecutor = new HandlerExecutor(new Handler(looper)); } @Override @@ -909,6 +1017,28 @@ final class BatteryController { public int getBatteryLevel(String address) { return getBluetoothDevice(mContext, address).getBatteryLevel(); } + + @Override + public void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().addOnMetadataChangedListener( + getBluetoothDevice(mContext, address), mExecutor, + listener); + } + + @Override + public void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().removeOnMetadataChangedListener( + getBluetoothDevice(mContext, address), listener); + } + + @Override + public byte[] getMetadata(String address, int key) { + return getBluetoothDevice(mContext, address).getMetadata(key); + } } // Helper class that adds copying and printing functionality to IInputDeviceBatteryState. 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 38847f284f505..3ce747f145dc2 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -16,7 +16,9 @@ package com.android.server.input +import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager import android.content.Context import android.content.ContextWrapper import android.hardware.BatteryState.STATUS_CHARGING @@ -246,6 +248,11 @@ class BatteryControllerTests { notifyDeviceChanged(deviceId, hasBattery, supportsUsi) } + private fun createBluetoothDevice(address: String): BluetoothDevice { + return context.getSystemService(BluetoothManager::class.java)!! + .adapter.getRemoteDevice(address) + } + @After fun tearDown() { InputManager.clearInstance() @@ -656,11 +663,13 @@ class BatteryControllerTests { addInputDevice(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - // Ensure that a BT battery listener is not added when there are no monitored BT devices. + // Listen to a non-Bluetooth device and ensure that the BT battery listener is not added + // when there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) verify(bluetoothBatteryManager, never()).addBatteryListener(any()) val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) - val listener = createMockListener() // The BT battery listener is added when the first BT input device is monitored. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) @@ -761,4 +770,126 @@ class BatteryControllerTests { BluetoothDevice.BATTERY_LEVEL_UNKNOWN) listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) } + + @Test + fun testRegisterBluetoothMetadataListenerForMonitoredBluetoothDevices() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn("11:22:33:44:55:66") + addInputDevice(BT_DEVICE_ID) + testLooper.dispatchNext() + addInputDevice(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + + // Listen to a non-Bluetooth device and ensure that the metadata listener is not added when + // there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addMetadataListener(any(), any()) + + val metadataListener1 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener2 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + + // The metadata listener is added when the first BT input device is monitored. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener1.capture()) + + // There is one metadata listener added for each BT device. + batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("11:22:33:44:55:66"), metadataListener2.capture()) + + // The metadata listener is removed when the device is no longer monitored. + batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .removeMetadataListener("AA:BB:CC:DD:EE:FF", metadataListener1.value) + + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn(null) + notifyDeviceChanged(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + verify(bluetoothBatteryManager) + .removeMetadataListener("11:22:33:44:55:66", metadataListener2.value) + } + + @Test + fun testNotifiesBluetoothMetadataBatteryChanges() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("21".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f, status = STATUS_UNKNOWN) + + // When the state has not changed, the listener is not notified again. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "21".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "25".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_UNKNOWN) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "true".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_CHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "false".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_DISCHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, null) + listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.25f, + status = STATUS_UNKNOWN) + } + + @Test + fun testBluetoothMetadataBatteryIsPrioritized() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("22".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) + + // A change in the Bluetooth battery level has no effect while there is a valid battery + // level obtained through the metadata. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 23) + listener.verifyNotified(BT_DEVICE_ID, mode = never(), capacity = 0.23f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "24".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.24f) + + // When the battery level from the metadata is no longer valid, we fall back to using the + // Bluetooth battery level. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, null) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.23f) + } }