Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1f64ce02 authored by Prabir Pradhan's avatar Prabir Pradhan
Browse files

BatteryController: Listen to battery changes from Bluetooth metadata

Some Bluetooth devices report battery state through Bluetooth metadata,
which can be obtained through BluetoothDevice#getMetadata. This is
another channel through which battery information can be obtained, and
is different than the BluetoothDevice#getBatteryLevel API which reports
the values from the Bluetooth Hands-Free Protocol (HFP).

In this CL, we start listening to Bluetooth metadata changes when a
Bluetooth input device is added.

In particular, we parse the following two keys:
- METADATA_MAIN_BATTERY - the battery level of the device
- METADATA_MAIN_CHARGING - whether or not the battery is being charged

To start listening to changes in metadata values, we add an
OnMetadataChangedListener for each monitored Bluetooth input device.

We prioritize the battery level from the metadata over the battery level
obtained from the BluetoothDevice#getBatteryLevel API.

Bug: 243005009
Test: atest BatteryControllerTests
Change-Id: I28d77e660b18821b14a67798fb88a5e926a7bebc
parent 95b9a3dd
Loading
Loading
Loading
Loading
+145 −15
Original line number Diff line number Diff line
@@ -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.
+133 −2
Original line number Diff line number Diff line
@@ -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)
    }
}