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

Commit 6ac41ac3 authored by Eric Laurent's avatar Eric Laurent
Browse files

AudioDeviceBroker: fix communication route when device is disconnected

When a device selected by a communication route client is disconnected,
do not remove the communication client from the stack, but mark it
as disabled instead.

If a device of the same type is reconnected, re-enable the client.

Bug: 383642693
Test: repro steps in bug
Flag: EXEMPT bug fix
Change-Id: I778d7ac05e67f4d93a3173cadb77e69425b0423d
parent afd3be1f
Loading
Loading
Loading
Loading
+127 −50
Original line number Diff line number Diff line
@@ -16,6 +16,22 @@
package com.android.server.audio;

import static android.media.audio.Flags.scoManagedByAudio;
import static android.media.AudioSystem.DEVICE_IN_ALL_SCO_SET;
import static android.media.AudioSystem.DEVICE_IN_BLE_HEADSET;
import static android.media.AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
import static android.media.AudioSystem.DEVICE_IN_USB_HEADSET;
import static android.media.AudioSystem.DEVICE_IN_WIRED_HEADSET;
import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO_SET;
import static android.media.AudioSystem.DEVICE_OUT_BLE_HEADSET;
import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_SCO;
import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT;
import static android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET;
import static android.media.AudioSystem.DEVICE_OUT_BUS;
import static android.media.AudioSystem.DEVICE_OUT_EARPIECE;
import static android.media.AudioSystem.DEVICE_OUT_SPEAKER;
import static android.media.AudioSystem.DEVICE_OUT_USB_HEADSET;
import static android.media.AudioSystem.DEVICE_OUT_WIRED_HEADSET;
import static android.media.AudioSystem.isBluetoothScoOutDevice;

import static com.android.media.audio.Flags.equalScoLeaVcIndexRange;
import static com.android.media.audio.Flags.optimizeBtDeviceSwitch;
@@ -78,9 +94,11 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
@@ -514,7 +532,7 @@ public class AudioDeviceBroker {
    @GuardedBy("mDeviceStateLock")
    private CommunicationRouteClient topCommunicationRouteClient() {
        for (CommunicationRouteClient crc : mCommunicationRouteClients) {
            if (crc.getUid() == mAudioModeOwner.mUid) {
            if (crc.getUid() == mAudioModeOwner.mUid && !crc.isDisabled()) {
                return crc;
            }
        }
@@ -574,36 +592,6 @@ public class AudioDeviceBroker {
        return false;
    }

    /*package */
    void postCheckCommunicationDeviceRemoval(@NonNull AudioDeviceAttributes device) {
        if (!isValidCommunicationDeviceType(
                AudioDeviceInfo.convertInternalDeviceToDeviceType(device.getInternalType()))) {
            return;
        }
        sendLMsgNoDelay(MSG_L_CHECK_COMMUNICATION_DEVICE_REMOVAL, SENDMSG_QUEUE, device);
    }

    @GuardedBy("mDeviceStateLock")
    void onCheckCommunicationDeviceRemoval(@NonNull AudioDeviceAttributes device) {
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "onCheckCommunicationDeviceRemoval device: " + device.toString());
        }
        for (CommunicationRouteClient crc : mCommunicationRouteClients) {
            if (device.equals(crc.getDevice())) {
                if (AudioService.DEBUG_COMM_RTE) {
                    Log.v(TAG, "onCheckCommunicationDeviceRemoval removing client: "
                            + crc.toString());
                }
                // Cancelling the route for this client will remove it from the stack and update
                // the communication route.
                CommunicationDeviceInfo deviceInfo = new CommunicationDeviceInfo(
                        crc.getBinder(), crc.getAttributionSource(), device, false,
                        BtHelper.SCO_MODE_UNDEFINED, "onCheckCommunicationDeviceRemoval",
                        crc.isPrivileged());
                postSetCommunicationDeviceForClient(deviceInfo);
            }
        }
    }

    // check playback or record activity after 6 seconds for UIDs
    private static final int CHECK_CLIENT_STATE_DELAY_MS = 6000;
@@ -1693,8 +1681,18 @@ public class AudioDeviceBroker {
                                boolean connect, @Nullable BluetoothDevice btDevice,
                                boolean deviceSwitch) {
        synchronized (mDeviceStateLock) {
            return mDeviceInventory.handleDeviceConnection(
            boolean status =  mDeviceInventory.handleDeviceConnection(
                    attributes, connect, false /*for test*/, btDevice, deviceSwitch);
            if (isValidCommunicationDeviceType(attributes.getType())
                    || mDuplexCommunicationDevices.containsValue(attributes.getInternalType())) {
                checkCommunicationRouteClientsDevices();
                if (connect || !deviceSwitch) {
                    onUpdateCommunicationRouteClient(
                            bluetoothScoRequestOwnerAttributionSource(),
                            "handleDeviceConnection");
                }
            }
            return status;
        }
    }

@@ -1953,12 +1951,13 @@ public class AudioDeviceBroker {
                                                || btInfo.mIsLeOutput)
                                            ? mAudioService.getBluetoothContextualVolumeStream()
                                            : AudioSystem.STREAM_DEFAULT);
                                if ((btInfo.mProfile == BluetoothProfile.LE_AUDIO
                                if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
                                        || btInfo.mProfile == BluetoothProfile.HEARING_AID
                                        || (mScoManagedByAudio
                                            && btInfo.mProfile == BluetoothProfile.HEADSET))
                                        && (btInfo.mState == BluetoothProfile.STATE_CONNECTED
                                            || !btInfo.mIsDeviceSwitch)) {
                                            && btInfo.mProfile == BluetoothProfile.HEADSET)) {
                                    checkCommunicationRouteClientsDevices();
                                    if (btInfo.mState == BluetoothProfile.STATE_CONNECTED
                                            || !btInfo.mIsDeviceSwitch) {
                                        onUpdateCommunicationRouteClient(
                                            bluetoothScoRequestOwnerAttributionSource(),
                                            "setBluetoothActiveDevice");
@@ -1966,6 +1965,7 @@ public class AudioDeviceBroker {
                                }
                            }
                        }
                    }
                } break;
                case MSG_BT_HEADSET_CNCT_FAILED:
                    synchronized (mSetModeLock) {
@@ -2123,13 +2123,6 @@ public class AudioDeviceBroker {
                    final BluetoothDevice btDevice = (BluetoothDevice) msg.obj;
                    BtHelper.onNotifyPreferredAudioProfileApplied(btDevice);
                } break;
                case MSG_L_CHECK_COMMUNICATION_DEVICE_REMOVAL: {
                    synchronized (mSetModeLock) {
                        synchronized (mDeviceStateLock) {
                            onCheckCommunicationDeviceRemoval((AudioDeviceAttributes) msg.obj);
                        }
                    }
                } break;
                case MSG_PERSIST_AUDIO_DEVICE_SETTINGS:
                    onPersistAudioDeviceSettings();
                    break;
@@ -2227,7 +2220,6 @@ public class AudioDeviceBroker {
    private static final int MSG_IIL_BTLEAUDIO_TIMEOUT = 49;

    private static final int MSG_L_NOTIFY_PREFERRED_AUDIOPROFILE_APPLIED = 52;
    private static final int MSG_L_CHECK_COMMUNICATION_DEVICE_REMOVAL = 53;

    private static final int MSG_PERSIST_AUDIO_DEVICE_SETTINGS = 54;

@@ -2431,18 +2423,21 @@ public class AudioDeviceBroker {
        private final IBinder mCb;
        @NonNull private final AttributionSource mAttributionSource;
        private final boolean mIsPrivileged;
        private AudioDeviceAttributes mDevice;
        @NonNull private AudioDeviceAttributes mDevice;
        private boolean mPlaybackActive;
        private boolean mRecordingActive;

        private boolean mDisabled;

        CommunicationRouteClient(IBinder cb, @NonNull AttributionSource attributionSource,
                AudioDeviceAttributes device, boolean isPrivileged) {
                @NonNull AudioDeviceAttributes device, boolean isPrivileged) {
            mCb = cb;
            mAttributionSource = attributionSource;
            mDevice = device;
            mIsPrivileged = isPrivileged;
            mPlaybackActive = mAudioService.isPlaybackActiveForUid(attributionSource.getUid());
            mRecordingActive = mAudioService.isRecordingActiveForUid(attributionSource.getUid());
            mDisabled = false;
        }

        public boolean registerDeathRecipient() {
@@ -2485,7 +2480,10 @@ public class AudioDeviceBroker {
            return mIsPrivileged;
        }

        AudioDeviceAttributes getDevice() {
        void setDevice(@NonNull AudioDeviceAttributes device) {
            mDevice = device;
        }
        @NonNull AudioDeviceAttributes getDevice() {
            return mDevice;
        }

@@ -2498,7 +2496,14 @@ public class AudioDeviceBroker {
        }

        public boolean isActive() {
            return mIsPrivileged || mRecordingActive || mPlaybackActive;
            return !mDisabled && (mIsPrivileged || mRecordingActive || mPlaybackActive);
        }

        public void setDisabled(boolean disabled) {
            mDisabled = disabled;
        }
        public boolean isDisabled() {
            return mDisabled;
        }

        @Override
@@ -2507,7 +2512,8 @@ public class AudioDeviceBroker {
                    + " mDevice: " + mDevice.toString()
                    + " mIsPrivileged: " + mIsPrivileged
                    + " mPlaybackActive: " + mPlaybackActive
                    + " mRecordingActive: " + mRecordingActive + "]";
                    + " mRecordingActive: " + mRecordingActive
                    + " mDisabled: " + mDisabled + "]";
        }
    }

@@ -2593,6 +2599,76 @@ public class AudioDeviceBroker {
        onUpdatePhoneStrategyDevice(preferredCommunicationDevice);
    }

    // Pairs of input and output devices for duplex communication devices (headsets)
    private final HashMap<Integer, Integer> mDuplexCommunicationDevices = new HashMap<>(
            Map.of(DEVICE_OUT_BLE_HEADSET, DEVICE_IN_BLE_HEADSET,
                DEVICE_OUT_WIRED_HEADSET, DEVICE_IN_WIRED_HEADSET,
                DEVICE_OUT_USB_HEADSET, DEVICE_IN_USB_HEADSET,
                DEVICE_OUT_BLUETOOTH_SCO, DEVICE_IN_BLUETOOTH_SCO_HEADSET,
                DEVICE_OUT_BLUETOOTH_SCO_HEADSET, DEVICE_IN_BLUETOOTH_SCO_HEADSET,
                DEVICE_OUT_BLUETOOTH_SCO_CARKIT, DEVICE_IN_BLUETOOTH_SCO_HEADSET
            ));
    /**
     * Scan communication route clients and disable them if their selected device is not connected
     * or re-enable them if a device of the same type as their connected device is connected
     */
    // @GuardedBy("mSetModeLock")
    @GuardedBy("mDeviceStateLock")
    private void checkCommunicationRouteClientsDevices() {
        for (CommunicationRouteClient crc : mCommunicationRouteClients) {
            int deviceType = crc.getDevice().getInternalType();
            // Skip non detachable devices
            if (deviceType == DEVICE_OUT_EARPIECE || deviceType == DEVICE_OUT_SPEAKER
                    || deviceType == DEVICE_OUT_BUS) {
                continue;
            }

            // outDeviceSet is the expected connected output device types for the requested device
            Set<Integer> outDeviceSet = null;
            // inDeviceSet is the expected input device for outDeviceSet. Null for non
            // duplex devices
            Set<Integer> inDeviceSet = null;
            // Special case for SCO because several device types are equivalent
            if (isBluetoothScoOutDevice(deviceType)) {
                outDeviceSet = DEVICE_OUT_ALL_SCO_SET;
                inDeviceSet = DEVICE_IN_ALL_SCO_SET;
            } else {
                outDeviceSet = new HashSet<>();
                outDeviceSet.add(deviceType);
                if (mDuplexCommunicationDevices.containsKey(deviceType)) {
                    inDeviceSet = new HashSet<>();
                    inDeviceSet.add(mDuplexCommunicationDevices.get(deviceType));
                }
            }

            AudioDeviceAttributes outAda =
                    mDeviceInventory.getFirstConnectedDeviceAttributesOfTypes(outDeviceSet);
            AudioDeviceAttributes inAda = (inDeviceSet == null) ? null
                    : mDeviceInventory.getFirstConnectedDeviceAttributesOfTypes(inDeviceSet);

            // A device is fully connected if the output device is connect and if not duplex
            // or an input device with the same address is connected
            boolean fullyConnected = outAda != null && (inDeviceSet == null
                    || (inAda != null && inAda.getAddress().equals(outAda.getAddress())));

            if (fullyConnected) {
                crc.setDevice(outAda);
                if (crc.isDisabled()) {
                    crc.setDisabled(false);
                    if (AudioService.DEBUG_COMM_RTE) {
                        Log.v(TAG,
                                "checkCommunicationRouteClientsDevices, enabling client: " + crc);
                    }
                }
            } else if (!crc.isDisabled()) {
                crc.setDisabled(true);
                if (AudioService.DEBUG_COMM_RTE) {
                    Log.v(TAG, "checkCommunicationRouteClientsDevices, disabling client: " + crc);
                }
            }
        }
    }

    /**
     * Select new communication device from communication route client at the top of the stack
     * and restore communication route including restarting SCO audio if needed.
@@ -2601,6 +2677,7 @@ public class AudioDeviceBroker {
    @GuardedBy("mDeviceStateLock")
    private void onUpdateCommunicationRouteClient(
            @Nullable AttributionSource previousBtScoRequesterAS, String eventSource) {

        CommunicationRouteClient crc = topCommunicationRouteClient();
        if (AudioService.DEBUG_COMM_RTE) {
            Log.v(TAG, "onUpdateCommunicationRouteClient, crc: " + crc
+17 −12
Original line number Diff line number Diff line
@@ -1867,7 +1867,6 @@ public class AudioDeviceInventory {
                        deviceSwitch);
                // always remove even if disconnection failed
                mConnectedDevices.remove(deviceKey);
                mDeviceBroker.postCheckCommunicationDeviceRemoval(attributes);
                status = true;
            }
            if (status) {
@@ -2413,7 +2412,7 @@ public class AudioDeviceInventory {
        } else {
            AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
                    "A2DP device addr=" + Utils.anonymizeBluetoothAddress(address)
                            + " made unavailable, deviceSwitch" + deviceSwitch))
                            + " made unavailable, deviceSwitch: " + deviceSwitch))
                    .printSlog(EventLogger.Event.ALOGI, TAG));
        }
        mApmConnectedDevices.remove(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
@@ -2423,7 +2422,6 @@ public class AudioDeviceInventory {
        mmi.record();
        updateBluetoothPreferredModes_l(null /*connectedDevice*/);
        purgeDevicesRoles_l();
        mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
    }

    @GuardedBy("mDevicesLock")
@@ -2479,7 +2477,6 @@ public class AudioDeviceInventory {
        // always remove regardless of the result
        mConnectedDevices.remove(
                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_IN_BLUETOOTH_A2DP, address));
        mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
    }

    @GuardedBy("mDevicesLock")
@@ -2541,7 +2538,6 @@ public class AudioDeviceInventory {
                .set(MediaMetrics.Property.DEVICE,
                        AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
                .record();
        mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
    }

    @GuardedBy("mDevicesLock")
@@ -2571,6 +2567,15 @@ public class AudioDeviceInventory {
        return devices.isEmpty() ? null : devices.get(0);
    }

    /**
     * Returns a DeviceInfo for the first connected device matching one of the supplied types
     */
    AudioDeviceAttributes getFirstConnectedDeviceAttributesOfTypes(Set<Integer> internalTypes) {
        DeviceInfo di = getFirstConnectedDeviceOfTypes(internalTypes);
        return di == null ? null : new AudioDeviceAttributes(
                di.mDeviceType, di.mDeviceAddress, di.mDeviceName);
    }

    /**
     * Returns a list of connected devices matching one of the supplied types
     */
@@ -2689,13 +2694,16 @@ public class AudioDeviceInventory {

            if (res != AudioSystem.AUDIO_STATUS_OK) {
                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                        "APM failed to make unavailable LE Audio device addr=" + address
                        "APM failed to make unavailable LE Audio "
                        + (AudioSystem.isInputDevice(device) ? "source" : "sink")
                        + " device addr=" + address
                        + " error=" + res).printSlog(EventLogger.Event.ALOGE, TAG));
                // not taking further action: proceeding as if disconnection from APM worked
            } else {
                AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
                        "LE Audio device addr=" + Utils.anonymizeBluetoothAddress(address)
                            + " made unavailable, deviceSwitch" + deviceSwitch)
                        "LE Audio " + (AudioSystem.isInputDevice(device) ? "source" : "sink")
                            + "device addr=" + Utils.anonymizeBluetoothAddress(address)
                            + " made unavailable, deviceSwitch: " + deviceSwitch)
                        .printSlog(EventLogger.Event.ALOGI, TAG));
            }
            mConnectedDevices.remove(DeviceInfo.makeDeviceListKey(device, address));
@@ -2704,9 +2712,6 @@ public class AudioDeviceInventory {
        setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/);
        updateBluetoothPreferredModes_l(null /*connectedDevice*/);
        purgeDevicesRoles_l();
        if (ada != null) {
            mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
        }
    }

    @GuardedBy("mDevicesLock")