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

Commit 854d4bde authored by Prabir Pradhan's avatar Prabir Pradhan
Browse files

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
parent bc1cd9c1
Loading
Loading
Loading
Loading
+212 −21
Original line number Diff line number Diff line
@@ -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;
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -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)
+133 −2

File changed.

Preview size limit exceeded, changes collapsed.