Loading android/app/src/com/android/bluetooth/a2dp/A2dpService.java +102 −17 Original line number Diff line number Diff line Loading @@ -42,6 +42,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; Loading Loading @@ -99,6 +101,8 @@ public class A2dpService extends ProfileService { @GuardedBy("mStateMachines") private BluetoothDevice mActiveDevice; private BluetoothDevice mExposedActiveDevice; private final ConcurrentMap<BluetoothDevice, A2dpStateMachine> mStateMachines = new ConcurrentHashMap<>(); Loading @@ -116,6 +120,8 @@ public class A2dpService extends ProfileService { boolean mA2dpOffloadEnabled = false; private BroadcastReceiver mBondStateChangedReceiver; private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); A2dpService() { mNativeInterface = requireNonNull(A2dpNativeInterface.getInstance()); Loading Loading @@ -196,10 +202,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; Loading @@ -213,12 +222,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; Loading Loading @@ -506,10 +518,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. Loading Loading @@ -590,15 +600,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; Loading Loading @@ -1052,10 +1061,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 + ")"); } Loading @@ -1064,9 +1152,6 @@ public class A2dpService extends ProfileService { if (mFactory.getAvrcpTargetService() != null) { mFactory.getAvrcpTargetService().handleA2dpActiveDeviceChanged(device); } synchronized (mStateMachines) { mActiveDevice = device; } mAdapterService .getActiveDeviceManager() Loading android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java +36 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading @@ -135,6 +137,7 @@ public class A2dpServiceTest { mConnectionStateChangedQueue.clear(); mAudioStateChangedQueue.clear(); mCodecConfigChangedQueue.clear(); mActiveDeviceQueue.clear(); TestUtils.clearAdapterService(mAdapterService); } Loading Loading @@ -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()); } } } } Loading Loading @@ -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()); Loading Loading @@ -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; Loading Loading
android/app/src/com/android/bluetooth/a2dp/A2dpService.java +102 −17 Original line number Diff line number Diff line Loading @@ -42,6 +42,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; Loading Loading @@ -99,6 +101,8 @@ public class A2dpService extends ProfileService { @GuardedBy("mStateMachines") private BluetoothDevice mActiveDevice; private BluetoothDevice mExposedActiveDevice; private final ConcurrentMap<BluetoothDevice, A2dpStateMachine> mStateMachines = new ConcurrentHashMap<>(); Loading @@ -116,6 +120,8 @@ public class A2dpService extends ProfileService { boolean mA2dpOffloadEnabled = false; private BroadcastReceiver mBondStateChangedReceiver; private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); A2dpService() { mNativeInterface = requireNonNull(A2dpNativeInterface.getInstance()); Loading Loading @@ -196,10 +202,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; Loading @@ -213,12 +222,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; Loading Loading @@ -506,10 +518,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. Loading Loading @@ -590,15 +600,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; Loading Loading @@ -1052,10 +1061,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 + ")"); } Loading @@ -1064,9 +1152,6 @@ public class A2dpService extends ProfileService { if (mFactory.getAvrcpTargetService() != null) { mFactory.getAvrcpTargetService().handleA2dpActiveDeviceChanged(device); } synchronized (mStateMachines) { mActiveDevice = device; } mAdapterService .getActiveDeviceManager() Loading
android/app/tests/unit/src/com/android/bluetooth/a2dp/A2dpServiceTest.java +36 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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); Loading @@ -135,6 +137,7 @@ public class A2dpServiceTest { mConnectionStateChangedQueue.clear(); mAudioStateChangedQueue.clear(); mCodecConfigChangedQueue.clear(); mActiveDeviceQueue.clear(); TestUtils.clearAdapterService(mAdapterService); } Loading Loading @@ -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()); } } } } Loading Loading @@ -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()); Loading Loading @@ -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; Loading