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

Commit 7b45f311 authored by Grzegorz Kołodziejczyk's avatar Grzegorz Kołodziejczyk Committed by Grzegorz Kolodziejczyk
Browse files

a2dp: Notify about active device change when A2DP audio device is added

Motivation for such change is ommision of transition to primary speaker
device while switching from LE Audio Bluetooth device to A2DP Bluetooth
device.

Every Bluetooth device change have two interesting for this case steps.
First step is to notify AudioManager about device being available.
Second step is to notify about device activity. Previous implementation
first notifies about device activity which in fact is not available in
AudioManager, what leads to disconnection previous device (LE Audio).
In such moment there are no available Bluetooth devices in AudioManager.
Lack of Bluetooth device in AudioManager leads to primary speaker device
switch.

Checked solution assumes that A2DP device would be available for
AudioManager then from AudioManager add device callback a notifiaction
will be sent. This soultion would protect from short switch to primary
speaker device.

Tag: #feature
Bug: 284432213
Test: atest A2dpServiceTest
Change-Id: I8f7905633dc7a19654f3b6455ee2e92c550d96da
parent 50d371f3
Loading
Loading
Loading
Loading
+102 −17
Original line number Diff line number Diff line
@@ -44,6 +44,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.BluetoothProfileConnectionInfo;
import android.os.Binder;
@@ -101,6 +103,8 @@ public class A2dpService extends ProfileService {

    @GuardedBy("mStateMachines")
    private BluetoothDevice mActiveDevice;

    private BluetoothDevice mExposedActiveDevice;
    private final ConcurrentMap<BluetoothDevice, A2dpStateMachine> mStateMachines =
            new ConcurrentHashMap<>();

@@ -118,6 +122,8 @@ public class A2dpService extends ProfileService {
    boolean mA2dpOffloadEnabled = false;

    private BroadcastReceiver mBondStateChangedReceiver;
    private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback =
            new AudioManagerAudioDeviceCallback();

    A2dpService() {
        mNativeInterface = requireNonNull(A2dpNativeInterface.getInstance());
@@ -198,10 +204,13 @@ public class A2dpService extends ProfileService {
        mBondStateChangedReceiver = new BondStateChangedReceiver();
        registerReceiver(mBondStateChangedReceiver, filter);

        // Step 8: Mark service as started
        // Step 8: Register Audio Device callback
        mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler);

        // Step 9: Mark service as started
        setA2dpService(this);

        // Step 9: Clear active device
        // Step 10: Clear active device
        removeActiveDevice(false);

        return true;
@@ -215,12 +224,15 @@ public class A2dpService extends ProfileService {
            return true;
        }

        // Step 9: Clear active device and stop playing audio
        // Step 10: Clear active device and stop playing audio
        removeActiveDevice(true);

        // Step 8: Mark service as stopped
        // Step 9: Mark service as stopped
        setA2dpService(null);

        // Step 8: Unregister Audio Device Callback
        mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback);

        // Step 7: Unregister broadcast receivers
        unregisterReceiver(mBondStateChangedReceiver);
        mBondStateChangedReceiver = null;
@@ -508,10 +520,8 @@ public class A2dpService extends ProfileService {
            synchronized (mStateMachines) {
                if (mActiveDevice == null) return true;
                previousActiveDevice = mActiveDevice;
                mActiveDevice = null;
            }

            // This needs to happen before we inform the audio manager that the device
            // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why.
            updateAndBroadcastActiveDevice(null);

            // Make sure the Audio Manager knows the previous active device is no longer active.
@@ -592,15 +602,14 @@ public class A2dpService extends ProfileService {
                    return false;
                }
                previousActiveDevice = mActiveDevice;
                mActiveDevice = device;
            }

            // Switch from one A2DP to another A2DP device
            if (DBG) {
                Log.d(TAG, "Switch A2DP devices to " + device + " from " + previousActiveDevice);
            }
            // This needs to happen before we inform the audio manager that the device
            // disconnected. Please see comment in updateAndBroadcastActiveDevice() for why.
            updateAndBroadcastActiveDevice(device);

            updateLowLatencyAudioSupport(device);

            BluetoothDevice newActiveDevice = null;
@@ -1054,10 +1063,89 @@ public class A2dpService extends ProfileService {
        }
    }

    // This needs to run before any of the Audio Manager connection functions since
    // AVRCP needs to be aware that the audio device is changed before the Audio Manager
    // changes the volume of the output devices.
    private void updateAndBroadcastActiveDevice(BluetoothDevice device) {
    /* Notifications of audio device connection/disconn events. */
    private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback {
        @Override
        public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
            if (mAudioManager == null || mAdapterService == null) {
                Log.e(TAG, "Callback called when A2dpService is stopped");
                return;
            }

            synchronized (mStateMachines) {
                for (AudioDeviceInfo deviceInfo : addedDevices) {
                    if (deviceInfo.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
                        continue;
                    }

                    String address = deviceInfo.getAddress();
                    if (address.equals("00:00:00:00:00:00")) {
                        continue;
                    }

                    byte[] addressBytes = Utils.getBytesFromAddress(address);
                    BluetoothDevice device = mAdapterService.getDeviceFromByte(addressBytes);

                    if (DBG) {
                        Log.d(TAG, " onAudioDevicesAdded: " + device + ", device type: "
                                + deviceInfo.getType());
                    }

                    /* Don't expose already exposed active device */
                    if (device.equals(mExposedActiveDevice)) {
                        if (DBG) {
                            Log.d(TAG, " onAudioDevicesAdded: " + device + " is already exposed");
                        }
                        return;
                    }


                    if (!device.equals(mActiveDevice)) {
                        Log.e(TAG, "Added device does not match to the one activated here. ("
                                + device + " != " + mActiveDevice
                                + " / " + mActiveDevice+ ")");
                        continue;
                    }

                    mExposedActiveDevice = device;
                    updateAndBroadcastActiveDevice(device);
                    return;
                }
            }
        }

        @Override
        public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
            if (mAudioManager == null || mAdapterService == null) {
                Log.e(TAG, "Callback called when LeAudioService is stopped");
                return;
            }

            synchronized (mStateMachines) {
                for (AudioDeviceInfo deviceInfo : removedDevices) {
                    if (deviceInfo.getType() != AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
                        continue;
                    }

                    String address = deviceInfo.getAddress();
                    if (address.equals("00:00:00:00:00:00")) {
                        continue;
                    }

                    mExposedActiveDevice = null;

                    if (DBG) {
                        Log.d(TAG, " onAudioDevicesRemoved: " + address + ", device type: "
                                + deviceInfo.getType()
                                + ", mActiveDevice: " + mActiveDevice);
                    }
                }
            }
        }
    }

    @VisibleForTesting
    void updateAndBroadcastActiveDevice(BluetoothDevice device) {
        if (DBG) {
            Log.d(TAG, "updateAndBroadcastActiveDevice(" + device + ")");
        }
@@ -1066,9 +1154,6 @@ public class A2dpService extends ProfileService {
        if (mFactory.getAvrcpTargetService() != null) {
            mFactory.getAvrcpTargetService().handleA2dpActiveDeviceChanged(device);
        }
        synchronized (mStateMachines) {
            mActiveDevice = device;
        }

        mAdapterService
                .getActiveDeviceManager()
+36 −0
Original line number Diff line number Diff line
@@ -75,6 +75,7 @@ public class A2dpServiceTest {
    private final BlockingQueue<Intent> mConnectionStateChangedQueue = new LinkedBlockingQueue<>();
    private final BlockingQueue<Intent> mAudioStateChangedQueue = new LinkedBlockingQueue<>();
    private final BlockingQueue<Intent> mCodecConfigChangedQueue = new LinkedBlockingQueue<>();
    private final BlockingQueue<Intent> mActiveDeviceQueue = new LinkedBlockingQueue<>();

    @Mock private AdapterService mAdapterService;
    @Mock private ActiveDeviceManager mActiveDeviceManager;
@@ -117,6 +118,7 @@ public class A2dpServiceTest {
        filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED);
        filter.addAction(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED);
        filter.addAction(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED);
        filter.addAction(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
        mA2dpIntentReceiver = new A2dpIntentReceiver();
        mTargetContext.registerReceiver(mA2dpIntentReceiver, filter);

@@ -135,6 +137,7 @@ public class A2dpServiceTest {
        mConnectionStateChangedQueue.clear();
        mAudioStateChangedQueue.clear();
        mCodecConfigChangedQueue.clear();
        mActiveDeviceQueue.clear();
        TestUtils.clearAdapterService(mAdapterService);
    }

@@ -163,6 +166,13 @@ public class A2dpServiceTest {
                    Assert.fail("Cannot add Intent to the Codec Config queue: " + e.getMessage());
                }
            }
            if (BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED.equals(intent.getAction())) {
                try {
                    mActiveDeviceQueue.put(intent);
                } catch (InterruptedException e) {
                    Assert.fail("Cannot add Intent to the device queue: " + e.getMessage());
                }
            }
        }
    }

@@ -214,6 +224,13 @@ public class A2dpServiceTest {
        Assert.assertNull(intent);
    }

    private void veifyActiveDeviceIntent(int timeoutMs, BluetoothDevice device) {
        Intent intent = TestUtils.waitForIntent(timeoutMs, mActiveDeviceQueue);
        Assert.assertNotNull(intent);
        Assert.assertEquals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, intent.getAction());
        Assert.assertEquals(device, intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE));
    }

    @Test
    public void testGetA2dpService() {
        Assert.assertEquals(mA2dpService, A2dpService.getA2dpService());
@@ -940,6 +957,25 @@ public class A2dpServiceTest {
        connectDeviceWithCodecStatus(device, null);
    }

    @Test
    public void testActiveDevice() {
        connectDevice(mTestDevice);

        /* Trigger setting active device */
        doReturn(true).when(mMockNativeInterface).setActiveDevice(any(BluetoothDevice.class));
        Assert.assertTrue(mA2dpService.setActiveDevice(mTestDevice));

        /* Check if setting active devices sets right device */
        Assert.assertEquals(mTestDevice, mA2dpService.getActiveDevice());

        /* Since A2dpService called AudioManager - assume Audio manager calles properly callback
         * mAudioManager.onAudioDeviceAdded
         */
        mA2dpService.updateAndBroadcastActiveDevice(mTestDevice);

        veifyActiveDeviceIntent(TIMEOUT_MS, mTestDevice);
    }

    private void connectDeviceWithCodecStatus(BluetoothDevice device,
            BluetoothCodecStatus codecStatus) {
        A2dpStackEvent connCompletedEvent;