diff options
author | Jack He <siyuanh@google.com> | 2022-03-04 05:57:20 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2022-03-04 05:57:20 +0000 |
commit | 052dca98d686469f3592eceeff65c85ffe11dbbd (patch) | |
tree | 9e538f6bd3a06cdcb32f5a79624be60c8a89df50 | |
parent | de7f62fbbab048b36aecbc1bda96bd2f15e8211c (diff) | |
parent | 66f381d91f0282ceb630dd6028003697f57028d2 (diff) |
Merge "le_broadcast: Initial alignment of service with the API" am: da47f8d985 am: 51ec356fa1 am: 66f381d91f
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Bluetooth/+/1988850
Change-Id: I2cb557629d8dbccc375bae3198e71092002529a1
16 files changed, 936 insertions, 209 deletions
diff --git a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java index 6ac0a88ea8..121dc49320 100644 --- a/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +++ b/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java @@ -20,13 +20,19 @@ package com.android.bluetooth.le_audio; import static android.Manifest.permission.BLUETOOTH_CONNECT; import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; +import static com.android.bluetooth.Utils.enforceBluetoothPrivilegedPermission; + import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudio; +import android.bluetooth.BluetoothLeAudioContentMetadata; +import android.bluetooth.BluetoothLeBroadcastMetadata; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.bluetooth.BluetoothUuid; import android.bluetooth.IBluetoothLeAudio; +import android.bluetooth.IBluetoothLeBroadcastCallback; import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.Context; @@ -38,6 +44,8 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.ParcelUuid; +import android.os.RemoteCallbackList; +import android.os.RemoteException; import android.util.Log; import com.android.bluetooth.Utils; @@ -50,12 +58,15 @@ import com.android.bluetooth.vc.VolumeControlService; import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.SynchronousResultReceiver; +import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; /** @@ -88,6 +99,12 @@ public class LeAudioService extends ProfileService { */ private static final int ACTIVE_CONTEXTS_NONE = 0; + /* + * Brodcast profile used by the lower layers + */ + private static final int BROADCAST_PROFILE_SONIFICATION = 0; + private static final int BROADCAST_PROFILE_MEDIA = 1; + private AdapterService mAdapterService; private DatabaseManager mDatabaseManager; private HandlerThread mStateMachinesThread; @@ -101,6 +118,9 @@ public class LeAudioService extends ProfileService { @VisibleForTesting AudioManager mAudioManager; + @VisibleForTesting + RemoteCallbackList<IBluetoothLeBroadcastCallback> mBroadcastCallbacks; + private class LeAudioGroupDescriptor { LeAudioGroupDescriptor() { mIsConnected = false; @@ -139,6 +159,10 @@ public class LeAudioService extends ProfileService { private Handler mHandler = new Handler(Looper.getMainLooper()); private final Map<Integer, Integer> mBroadcastStateMap = new HashMap<>(); + final Map<Integer, Integer> mBroadcastIdMap = new HashMap<>(); + private final Map<Integer, Boolean> mBroadcastsPlaybackMap = new HashMap<>(); + private final List<BluetoothLeBroadcastMetadata> mBroadcastMetadataList = new ArrayList<>(); + @Override protected IProfileServiceBinder initBinder() { return new BluetoothLeAudioBinder(this); @@ -174,6 +198,10 @@ public class LeAudioService extends ProfileService { mDeviceGroupIdMap.clear(); mBroadcastStateMap.clear(); + mBroadcastIdMap.clear(); + mBroadcastMetadataList.clear(); + mBroadcastsPlaybackMap.clear(); + mGroupDescriptors.clear(); // Setup broadcast receivers @@ -188,6 +216,7 @@ public class LeAudioService extends ProfileService { // Initialize Broadcast native interface if (mAdapterService.isLeAudioBroadcastSourceSupported()) { + mBroadcastCallbacks = new RemoteCallbackList<IBluetoothLeBroadcastCallback>(); mLeAudioBroadcasterNativeInterface = Objects.requireNonNull( LeAudioBroadcasterNativeInterface.getInstance(), "LeAudioBroadcasterNativeInterface cannot be null when LeAudioService starts"); @@ -256,7 +285,15 @@ public class LeAudioService extends ProfileService { mDeviceGroupIdMap.clear(); mGroupDescriptors.clear(); + if (mBroadcastCallbacks != null) { + mBroadcastCallbacks.kill(); + } + mBroadcastStateMap.clear(); + mBroadcastIdMap.clear(); + mBroadcastsPlaybackMap.clear(); + mBroadcastMetadataList.clear(); + if (mLeAudioBroadcasterNativeInterface != null) { mLeAudioBroadcasterNativeInterface.cleanup(); mLeAudioBroadcasterNativeInterface = null; @@ -549,22 +586,13 @@ public class LeAudioService extends ProfileService { * @param audioProfile broadcast audio profile * @param broadcastCode optional code if broadcast should be encrypted */ - public void createBroadcast(byte[] metadata, int audioProfile, byte[] broadcastCode) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.createBroadcast(metadata, audioProfile, - broadcastCode); - } - } - - /** - * Updates LeAudio Broadcast instance metadata. - * @param instanceId broadcast instance identifier - * @param metadata metadata buffer with TLVs - */ - public void updateMetadata(int instanceId, byte[] metadata) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.updateMetadata(instanceId, metadata); + public void createBroadcast(BluetoothLeAudioContentMetadata metadata, byte[] broadcastCode) { + if (mLeAudioBroadcasterNativeInterface == null) { + Log.w(TAG, "Native interface not available."); + return; } + mLeAudioBroadcasterNativeInterface.createBroadcast(metadata.getRawMetadata(), + BROADCAST_PROFILE_MEDIA, broadcastCode); } /** @@ -572,29 +600,39 @@ public class LeAudioService extends ProfileService { * @param instanceId broadcast instance identifier */ public void startBroadcast(int instanceId) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.startBroadcast(instanceId); + if (mLeAudioBroadcasterNativeInterface == null) { + Log.w(TAG, "Native interface not available."); + return; } + if (DBG) Log.d(TAG, "startBroadcast"); + mLeAudioBroadcasterNativeInterface.startBroadcast(instanceId); } /** - * Stop LeAudio Broadcast instance. + * Updates LeAudio Broadcast instance metadata. * @param instanceId broadcast instance identifier + * @param metadata metadata for the default Broadcast subgroup */ - public void stopBroadcast(int instanceId) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.stopBroadcast(instanceId); + public void updateBroadcast(int instanceId, BluetoothLeAudioContentMetadata metadata) { + if (mLeAudioBroadcasterNativeInterface == null) { + Log.w(TAG, "Native interface not available."); + return; } + if (DBG) Log.d(TAG, "updateBroadcast"); + mLeAudioBroadcasterNativeInterface.updateMetadata(instanceId, metadata.getRawMetadata()); } /** - * Pause LeAudio Broadcast instance. + * Stop LeAudio Broadcast instance. * @param instanceId broadcast instance identifier */ - public void pauseBroadcast(int instanceId) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.pauseBroadcast(instanceId); + public void stopBroadcast(Integer instanceId) { + if (mLeAudioBroadcasterNativeInterface == null) { + Log.w(TAG, "Native interface not available."); + return; } + if (DBG) Log.d(TAG, "stopBroadcast"); + mLeAudioBroadcasterNativeInterface.stopBroadcast(instanceId); } /** @@ -602,9 +640,12 @@ public class LeAudioService extends ProfileService { * @param instanceId broadcast instance identifier */ public void destroyBroadcast(int instanceId) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.destroyBroadcast(instanceId); + if (mLeAudioBroadcasterNativeInterface == null) { + Log.w(TAG, "Native interface not available."); + return; } + if (DBG) Log.d(TAG, "destroyBroadcast"); + mLeAudioBroadcasterNativeInterface.destroyBroadcast(instanceId); } /** @@ -612,18 +653,37 @@ public class LeAudioService extends ProfileService { * @param instanceId broadcast instance identifier */ public void getBroadcastId(int instanceId) { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.getBroadcastId(instanceId); + if (mLeAudioBroadcasterNativeInterface == null) { + Log.w(TAG, "Native interface not available."); + return; } + mLeAudioBroadcasterNativeInterface.getBroadcastId(instanceId); } /** - * Get all LeAudio Broadcast instance states. + * Checks if Broadcast instance is playing. + * @param instanceId broadcast instance identifier + * @return true if if broadcast is playing, false otherwise */ - public void getAllBroadcastStates() { - if (mLeAudioBroadcasterNativeInterface != null) { - mLeAudioBroadcasterNativeInterface.getAllBroadcastStates(); - } + public boolean isPlaying(int instanceId) { + return mBroadcastsPlaybackMap.getOrDefault(instanceId, false); + } + + /** + * Get all broadcast metadata. + * @return list of all know Broadcast metadata + */ + public List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { + return mBroadcastMetadataList; + } + + /** + * Get the maximum number of supported simultaneous broadcasts. + * @return number of supported simultaneous broadcasts + */ + public int getMaximumNumberOfBroadcast() { + /* TODO: This is currently fixed to 1 */ + return 1; } private BluetoothDevice getFirstDeviceFromGroup(Integer groupId) { @@ -1048,40 +1108,102 @@ public class LeAudioService extends ProfileService { } } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_BROADCAST_CREATED) { - // TODO: Implement + int instanceId = stackEvent.valueInt1; + boolean success = stackEvent.valueBool1; + if (success) { + Log.d(TAG, "Broadcast Instance id: " + instanceId + " created."); + startBroadcast(instanceId); + getBroadcastId(instanceId); + } else { + // TODO: Improve reason reporting or extend the native stack event with reason code + notifyBroadcastStartFailed(instanceId, BluetoothStatusCodes.ERROR_UNKNOWN); + } + } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_BROADCAST_DESTROYED) { - // TODO: Implement + Integer instanceId = stackEvent.valueInt1; + + // TODO: Improve reason reporting or extend the native stack event with reason code + notifyOnBroadcastStopped(instanceId, BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + + mBroadcastStateMap.remove(instanceId); + mBroadcastsPlaybackMap.remove(instanceId); + if (mBroadcastIdMap.containsKey(instanceId)) { + Integer broadcastId = mBroadcastIdMap.get(instanceId); + mBroadcastMetadataList.removeIf(m -> broadcastId == m.getBroadcastId()); + mBroadcastIdMap.remove(instanceId); + } + } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_BROADCAST_STATE) { int instanceId = stackEvent.valueInt1; int state = stackEvent.valueInt2; + mBroadcastStateMap.put(instanceId, state); - if (state == LeAudioStackEvent.BROADCAST_STATE_STREAMING) { - mBroadcastStateMap.put(instanceId, state); - if (mBroadcastStateMap.size() == 1) { - if (!Objects.equals(device, mActiveAudioOutDevice)) { + if (state == LeAudioStackEvent.BROADCAST_STATE_STOPPED) { + if (DBG) Log.d(TAG, "Broadcast Instance id: " + instanceId + " stopped."); + destroyBroadcast(instanceId); + + } else if (state == LeAudioStackEvent.BROADCAST_STATE_CONFIGURING) { + if (DBG) Log.d(TAG, "Broadcast Instance id: " + instanceId + " configuring."); + + } else if (state == LeAudioStackEvent.BROADCAST_STATE_PAUSED) { + if (DBG) Log.d(TAG, "Broadcast Instance id: " + instanceId + " paused."); + + if (!mBroadcastsPlaybackMap.containsKey(instanceId)) { + // Initial playback state after the creation + notifyBroadcastStarted(instanceId, + BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + } + + // Playback paused + mBroadcastsPlaybackMap.put(instanceId, false); + notifyPlaybackStopped(instanceId, BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST); + + // Notify audio manager + if (Collections.frequency(mBroadcastsPlaybackMap.values(), true) == 0) { + if (Objects.equals(device, mActiveAudioOutDevice)) { BluetoothDevice previousDevice = mActiveAudioOutDevice; - mActiveAudioOutDevice = device; + mActiveAudioOutDevice = null; mAudioManager.handleBluetoothActiveDeviceChanged(mActiveAudioOutDevice, previousDevice, - BluetoothProfileConnectionInfo.createLeAudioInfo(false, true)); + // TODO: implement createLeAudioBroadcastInfo() + BluetoothProfileConnectionInfo.createLeAudioInfo(true, true)); } } - } else { - mBroadcastStateMap.remove(instanceId); - if (mBroadcastStateMap.size() == 0) { - if (Objects.equals(device, mActiveAudioOutDevice)) { + + } else if (state == LeAudioStackEvent.BROADCAST_STATE_STOPPING) { + if (DBG) Log.d(TAG, "Broadcast Instance id: " + instanceId + " stopping."); + + } else if (state == LeAudioStackEvent.BROADCAST_STATE_STREAMING) { + if (DBG) Log.d(TAG, "Broadcast Instance id: " + instanceId + " streaming."); + + if (!mBroadcastsPlaybackMap.containsKey(instanceId)) { + notifyBroadcastStarted(instanceId, + BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST); + } + + // Stream resumed + mBroadcastsPlaybackMap.put(instanceId, true); + notifyPlaybackStarted(instanceId, BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST); + + // Notify audio manager + if (Collections.frequency(mBroadcastsPlaybackMap.values(), true) == 1) { + if (!Objects.equals(device, mActiveAudioOutDevice)) { BluetoothDevice previousDevice = mActiveAudioOutDevice; - mActiveAudioOutDevice = null; + mActiveAudioOutDevice = device; mAudioManager.handleBluetoothActiveDeviceChanged(mActiveAudioOutDevice, previousDevice, - BluetoothProfileConnectionInfo.createLeAudioInfo(true, true)); + // TODO: implement createLeAudioBroadcastInfo() + BluetoothProfileConnectionInfo.createLeAudioInfo(false, true)); } } } - // TODO: Implement + } else if (stackEvent.type == LeAudioStackEvent.EVENT_TYPE_BROADCAST_ID) { - // TODO: Implement + int instanceId = stackEvent.valueInt1; + byte[] broadcastId = stackEvent.valueByte1; + mBroadcastIdMap.put(instanceId, new BigInteger(broadcastId).intValue()); } + // TODO: Support Broadcast metadata updates if (intent != null) { sendBroadcast(intent, BLUETOOTH_CONNECT); @@ -1401,6 +1523,177 @@ public class LeAudioService extends ProfileService { } } + private void notifyBroadcastStarted(Integer instanceId, int reason) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onBroadcastStarted(reason, broadcastId); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyBroadcastStartFailed(Integer instanceId, int reason) { + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onBroadcastStartFailed(reason); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyOnBroadcastStopped(Integer instanceId, int reason) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onBroadcastStopped(reason, broadcastId); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyOnBroadcastStopFailed(int reason) { + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onBroadcastStopFailed(reason); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyPlaybackStarted(Integer instanceId, int reason) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onPlaybackStarted(reason, broadcastId); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyPlaybackStopped(Integer instanceId, int reason) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onPlaybackStopped(reason, broadcastId); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyBroadcastUpdated(int instanceId, int reason) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i).onBroadcastUpdated(reason, broadcastId); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyBroadcastUpdateFailed(int instanceId, int reason) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i) + .onBroadcastUpdateFailed(reason, broadcastId); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + + private void notifyBroadcastMetadataChanged(int instanceId, + BluetoothLeBroadcastMetadata metadata) { + if (!mBroadcastIdMap.containsKey(instanceId)) { + Log.e(TAG, "Unknown Broadcast ID for broadcast instance: " + instanceId); + return; + } + + Integer broadcastId = mBroadcastIdMap.get(instanceId); + if (mBroadcastCallbacks != null) { + int n = mBroadcastCallbacks.beginBroadcast(); + for (int i = 0; i < n; i++) { + try { + mBroadcastCallbacks.getBroadcastItem(i) + .onBroadcastMetadataChanged(broadcastId, metadata); + } catch (RemoteException e) { + continue; + } + } + mBroadcastCallbacks.finishBroadcast(); + } + } + /** * Binder object: must be a static class or memory leak may occur */ @@ -1638,84 +1931,131 @@ public class LeAudioService extends ProfileService { } @Override - public void createBroadcast(byte[] metadata, int audioProfile, byte[] broadcastCode, - AttributionSource source) { + public void registerLeBroadcastCallback(IBluetoothLeBroadcastCallback callback, + AttributionSource source, SynchronousResultReceiver receiver) { LeAudioService service = getService(source); - if (service == null) { + if ((service == null) || (service.mBroadcastCallbacks == null)) { + receiver.propagateException(new IllegalStateException("Service is unavailable")); return; } - service.createBroadcast(metadata, audioProfile, broadcastCode); + enforceBluetoothPrivilegedPermission(service); + try { + service.mBroadcastCallbacks.register(callback); + receiver.send(null); + } catch (RuntimeException e) { + receiver.propagateException(e); + } } @Override - public void updateMetadata(int instanceId, byte[] metadata, AttributionSource source) { + public void unregisterLeBroadcastCallback(IBluetoothLeBroadcastCallback callback, + AttributionSource source, SynchronousResultReceiver receiver) { LeAudioService service = getService(source); - if (service == null) { + if ((service == null) || (service.mBroadcastCallbacks == null)) { + receiver.propagateException(new IllegalStateException("Service is unavailable")); return; } - service.updateMetadata(instanceId, metadata); + enforceBluetoothPrivilegedPermission(service); + try { + service.mBroadcastCallbacks.unregister(callback); + receiver.send(null); + } catch (RuntimeException e) { + receiver.propagateException(e); + } } @Override - public void startBroadcast(int instanceId, AttributionSource source) { + public void startBroadcast(BluetoothLeAudioContentMetadata contentMetadata, + byte[] broadcastCode, AttributionSource source) { LeAudioService service = getService(source); - if (service == null) { - return; + if (service != null) { + service.createBroadcast(contentMetadata, broadcastCode); } - - service.startBroadcast(instanceId); } @Override - public void stopBroadcast(int instanceId, AttributionSource source) { + public void stopBroadcast(int broadcastId, AttributionSource source) { LeAudioService service = getService(source); - if (service == null) { - return; + if (service != null) { + Optional<Integer> instanceId = service.mBroadcastIdMap.entrySet() + .stream() + .filter(entry -> Objects.equals(entry.getValue(), broadcastId)) + .map(Map.Entry::getKey) + .findFirst(); + if (instanceId.isPresent()) { + service.stopBroadcast(instanceId.get()); + } } - - service.stopBroadcast(instanceId); } @Override - public void pauseBroadcast(int instanceId, AttributionSource source) { + public void updateBroadcast(int broadcastId, + BluetoothLeAudioContentMetadata contentMetadata, AttributionSource source) { LeAudioService service = getService(source); - if (service == null) { - return; + if (service != null) { + Optional<Integer> instanceId = service.mBroadcastIdMap.entrySet() + .stream() + .filter(entry -> Objects.equals(entry.getValue(), broadcastId)) + .map(Map.Entry::getKey) + .findFirst(); + if (instanceId.isPresent()) { + service.updateBroadcast(instanceId.get(), contentMetadata); + } } - - service.pauseBroadcast(instanceId); } @Override - public void destroyBroadcast(int instanceId, AttributionSource source) { - LeAudioService service = getService(source); - if (service == null) { - return; + public void isPlaying(int broadcastId, AttributionSource source, + SynchronousResultReceiver receiver) { + try { + boolean defaultValue = false; + LeAudioService service = getService(source); + if (service != null) { + Optional<Integer> instanceId = service.mBroadcastIdMap.entrySet() + .stream() + .filter(entry -> Objects.equals(entry.getValue(), broadcastId)) + .map(Map.Entry::getKey) + .findFirst(); + if (instanceId.isPresent()) { + defaultValue = service.isPlaying(instanceId.get()); + } + } + receiver.send(defaultValue); + } catch (RuntimeException e) { + receiver.propagateException(e); } - - service.destroyBroadcast(instanceId); } @Override - public void getBroadcastId(int instanceId, AttributionSource source) { - LeAudioService service = getService(source); - if (service == null) { - return; + public void getAllBroadcastMetadata(AttributionSource source, + SynchronousResultReceiver receiver) { + try { + List<BluetoothLeBroadcastMetadata> defaultValue = new ArrayList<>(); + LeAudioService service = getService(source); + if (service != null) { + defaultValue = service.getAllBroadcastMetadata(); + } + receiver.send(defaultValue); + } catch (RuntimeException e) { + receiver.propagateException(e); } - - service.getBroadcastId(instanceId); } @Override - public void getAllBroadcastStates(AttributionSource source) { - LeAudioService service = getService(source); - if (service == null) { - return; + public void getMaximumNumberOfBroadcast(AttributionSource source, + SynchronousResultReceiver receiver) { + try { + int defaultValue = 0; + LeAudioService service = getService(source); + if (service != null) { + defaultValue = service.getMaximumNumberOfBroadcast(); + } + receiver.send(defaultValue); + } catch (RuntimeException e) { + receiver.propagateException(e); } - - service.getAllBroadcastStates(); } } diff --git a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java index 70e1dee146..9dce7a6470 100644 --- a/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java +++ b/android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioBroadcastServiceTest.java @@ -150,85 +150,56 @@ public class LeAudioBroadcastServiceTest { @Test public void testCreateBroadcastNative() { int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); - verify(mNativeInterface, times(1)).createBroadcast(eq(meta), - eq(broadcast_profile), eq(code)); - } + BluetoothLeAudioContentMetadata.Builder meta_builder = + new BluetoothLeAudioContentMetadata.Builder(); + meta_builder.setLanguage("EN"); + meta_builder.setProgramInfo("Public broadcast info"); + BluetoothLeAudioContentMetadata meta = meta_builder.build(); + mService.createBroadcast(meta, code); - @Test - public void testStartBroadcastNative() { - int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; - byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); - - int broadcast_id = 243; - mService.startBroadcast(broadcast_id); - verify(mNativeInterface, times(1)).startBroadcast(eq(broadcast_id)); + verify(mNativeInterface, times(1)).createBroadcast(eq(meta.getRawMetadata()), eq(1), + eq(code)); } @Test - public void testStopBroadcastNative() { + public void testStartStopBroadcastNative() { int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); - int broadcast_id = 243; - mService.stopBroadcast(broadcast_id); - verify(mNativeInterface, times(1)).stopBroadcast(eq(broadcast_id)); - } + BluetoothLeAudioContentMetadata.Builder meta_builder = + new BluetoothLeAudioContentMetadata.Builder(); + meta_builder.setLanguage("EN"); + meta_builder.setProgramInfo("Public broadcast info"); + BluetoothLeAudioContentMetadata meta = meta_builder.build(); + mService.createBroadcast(meta, code); - @Test - public void testPauseBroadcastNative() { - int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; - byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); + int instance_id = 243; + mService.startBroadcast(instance_id); + verify(mNativeInterface, times(1)).startBroadcast(eq(instance_id)); - int broadcast_id = 243; - mService.pauseBroadcast(broadcast_id); - verify(mNativeInterface, times(1)).pauseBroadcast(eq(broadcast_id)); + mService.stopBroadcast(instance_id); + verify(mNativeInterface, times(1)).stopBroadcast(eq(instance_id)); } @Test public void testDestroyBroadcastNative() { int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); + + BluetoothLeAudioContentMetadata.Builder meta_builder = + new BluetoothLeAudioContentMetadata.Builder(); + meta_builder.setLanguage("ENG"); + meta_builder.setProgramInfo("Public broadcast info"); + BluetoothLeAudioContentMetadata meta = meta_builder.build(); + mService.createBroadcast(meta, code); int broadcast_id = 243; mService.destroyBroadcast(broadcast_id); verify(mNativeInterface, times(1)).destroyBroadcast(eq(broadcast_id)); } - - @Test - public void testGetBroadcastAddressNative() { - int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; - byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); - - int broadcast_id = 243; - mService.getBroadcastId(broadcast_id); - verify(mNativeInterface, times(1)).getBroadcastId(eq(broadcast_id)); - } - - @Test - public void testGetAllBroadcastStates() { - int broadcast_profile = 0; - byte[] meta = new byte[]{0x02, 0x01, 0x02}; - byte[] code = {0x00, 0x01, 0x00}; - mService.createBroadcast(meta, broadcast_profile, code); - - int broadcast_id = 243; - mService.getAllBroadcastStates(); - verify(mNativeInterface, times(1)).getAllBroadcastStates(); - } + // FIXME: Add the missign API test cases private class LeAudioIntentReceiver extends BroadcastReceiver { @Override diff --git a/framework/java/android/bluetooth/BluetoothAdapter.java b/framework/java/android/bluetooth/BluetoothAdapter.java index 15f0f923c1..936a08569b 100644 --- a/framework/java/android/bluetooth/BluetoothAdapter.java +++ b/framework/java/android/bluetooth/BluetoothAdapter.java @@ -3480,6 +3480,9 @@ public final class BluetoothAdapter { } else if (profile == BluetoothProfile.LE_AUDIO) { BluetoothLeAudio leAudio = new BluetoothLeAudio(context, listener, this); return true; + } else if (profile == BluetoothProfile.LE_AUDIO_BROADCAST) { + BluetoothLeBroadcast leAudio = new BluetoothLeBroadcast(context, listener); + return true; } else if (profile == BluetoothProfile.VOLUME_CONTROL) { BluetoothVolumeControl vcs = new BluetoothVolumeControl(context, listener, this); return true; @@ -3587,6 +3590,10 @@ public final class BluetoothAdapter { BluetoothLeAudio leAudio = (BluetoothLeAudio) proxy; leAudio.close(); break; + case BluetoothProfile.LE_AUDIO_BROADCAST: + BluetoothLeBroadcast leAudioBroadcast = (BluetoothLeBroadcast) proxy; + leAudioBroadcast.close(); + break; case BluetoothProfile.VOLUME_CONTROL: BluetoothVolumeControl vcs = (BluetoothVolumeControl) proxy; vcs.close(); diff --git a/framework/java/android/bluetooth/BluetoothLeBroadcast.java b/framework/java/android/bluetooth/BluetoothLeBroadcast.java index 4537cc6f97..5bf6e00fea 100644 --- a/framework/java/android/bluetooth/BluetoothLeBroadcast.java +++ b/framework/java/android/bluetooth/BluetoothLeBroadcast.java @@ -16,21 +16,33 @@ package android.bluetooth; +import static android.bluetooth.BluetoothUtils.getSyncTimeout; + import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.bluetooth.annotations.RequiresBluetoothConnectPermission; +import android.content.AttributionSource; import android.content.Context; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.CloseGuard; import android.util.Log; +import com.android.modules.utils.SynchronousResultReceiver; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; +import java.util.concurrent.TimeoutException; /** * This class provides the public APIs to control the BAP Broadcast Source profile. @@ -45,6 +57,148 @@ import java.util.concurrent.Executor; public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfile { private static final String TAG = "BluetoothLeBroadcast"; private static final boolean DBG = true; + private static final boolean VDBG = false; + + private CloseGuard mCloseGuard; + + private final BluetoothAdapter mAdapter; + private final AttributionSource mAttributionSource; + private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector = + new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO_BROADCAST, + "BluetoothLeAudioBroadcast", IBluetoothLeAudio.class.getName()) { + @Override + public IBluetoothLeAudio getServiceInterface(IBinder service) { + return IBluetoothLeAudio.Stub.asInterface(service); + } + }; + + private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>(); + + @SuppressLint("AndroidFrameworkBluetoothPermission") + private final IBluetoothLeBroadcastCallback mCallback = + new IBluetoothLeBroadcastCallback.Stub() { + @Override + public void onBroadcastStarted(int reason, int broadcastId) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastStarted(reason, broadcastId)); + } + } + + @Override + public void onBroadcastStartFailed(int reason) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastStartFailed(reason)); + } + } + + @Override + public void onBroadcastStopped(int reason, int broadcastId) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastStopped(reason, broadcastId)); + } + } + + @Override + public void onBroadcastStopFailed(int reason) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastStopFailed(reason)); + } + } + + @Override + public void onPlaybackStarted(int reason, int broadcastId) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onPlaybackStarted(reason, broadcastId)); + } + } + + @Override + public void onPlaybackStopped(int reason, int broadcastId) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onPlaybackStopped(reason, broadcastId)); + } + } + + @Override + public void onBroadcastUpdated(int reason, int broadcastId) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastUpdated(reason, broadcastId)); + } + } + + @Override + public void onBroadcastUpdateFailed(int reason, int broadcastId) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastUpdateFailed(reason, broadcastId)); + } + } + + @Override + public void onBroadcastMetadataChanged(int broadcastId, + BluetoothLeBroadcastMetadata metadata) { + for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry: + mCallbackExecutorMap.entrySet()) { + BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); + Executor executor = callbackExecutorEntry.getValue(); + executor.execute(() -> callback.onBroadcastMetadataChanged(broadcastId, metadata)); + } + } + }; + + @SuppressLint("AndroidFrameworkBluetoothPermission") + private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = + new IBluetoothStateChangeCallback.Stub() { + public void onBluetoothStateChange(boolean up) { + if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); + if (up) { + // re-register the service-to-app callback + synchronized (mCallbackExecutorMap) { + if (!mCallbackExecutorMap.isEmpty()) { + try { + final IBluetoothLeAudio service = getService(); + if (service != null) { + final SynchronousResultReceiver<Integer> recv = + new SynchronousResultReceiver(); + service.registerLeBroadcastCallback(mCallback, + mAttributionSource, recv); + recv.awaitResultNoInterrupt(getSyncTimeout()) + .getValue(null); + } + } catch (TimeoutException e) { + Log.e(TAG, "onBluetoothServiceUp: Failed to register " + + "Le Broadcaster callback", e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + } + }; /** * Interface for receiving events related to Broadcast Source @@ -171,7 +325,33 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi * @param listener listens for service callbacks across binder * @hide */ - /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) {} + /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) { + mAdapter = BluetoothAdapter.getDefaultAdapter(); + mAttributionSource = mAdapter.getAttributionSource(); + mProfileConnector.connect(context, listener); + + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + mCloseGuard = new CloseGuard(); + mCloseGuard.open("close"); + } + + /** + * @hide + */ + protected void finalize() { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } /** * Not supported since LE Audio Broadcasts do not establish a connection. @@ -246,8 +426,36 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } - log("registerCallback"); - throw new UnsupportedOperationException("Not Implemented"); + if (!isEnabled()) { + throw new IllegalStateException("service not enabled"); + } + + if (DBG) log("registerCallback"); + + synchronized (mCallbackExecutorMap) { + // If the callback map is empty, we register the service-to-app callback + if (mCallbackExecutorMap.isEmpty()) { + try { + final IBluetoothLeAudio service = getService(); + if (service != null) { + final SynchronousResultReceiver<Integer> recv = + new SynchronousResultReceiver(); + service.registerLeBroadcastCallback(mCallback, mAttributionSource, recv); + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); + } + } catch (TimeoutException | IllegalStateException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + // Adds the passed in callback to our map of callbacks to executors + if (mCallbackExecutorMap.containsKey(callback)) { + throw new IllegalArgumentException("This callback has already been registered"); + } + mCallbackExecutorMap.put(callback, executor); + } } /** @@ -271,8 +479,30 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi if (callback == null) { throw new IllegalArgumentException("callback cannot be null"); } - log("unregisterCallback"); - throw new UnsupportedOperationException("Not Implemented"); + + if (DBG) log("unregisterCallback"); + + synchronized (mCallbackExecutorMap) { + if (mCallbackExecutorMap.remove(callback) != null) { + throw new IllegalArgumentException("This callback has not been registered"); + } + } + + // If the callback map is empty, we unregister the service-to-app callback + if (mCallbackExecutorMap.isEmpty()) { + try { + final IBluetoothLeAudio service = getService(); + if (service != null) { + final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); + service.unregisterLeBroadcastCallback(mCallback, mAttributionSource, recv); + recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null); + } + } catch (TimeoutException | IllegalStateException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -323,6 +553,17 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi public void startBroadcast(@NonNull BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode) { if (DBG) log("startBroadcasting"); + final IBluetoothLeAudio service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + service.startBroadcast(contentMetadata, broadcastCode, mAttributionSource); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -346,7 +587,18 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi }) public void updateBroadcast(int broadcastId, @NonNull BluetoothLeAudioContentMetadata contentMetadata) { - + if (DBG) log("updateBroadcast"); + final IBluetoothLeAudio service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + service.updateBroadcast(broadcastId, contentMetadata, mAttributionSource); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -368,6 +620,17 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi }) public void stopBroadcast(int broadcastId) { if (DBG) log("disableBroadcastMode"); + final IBluetoothLeAudio service = getService(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + service.stopBroadcast(broadcastId, mAttributionSource); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -385,7 +648,23 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean isPlaying(int broadcastId) { - return false; + final IBluetoothLeAudio service = getService(); + final boolean defaultValue = false; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Boolean> recv = new SynchronousResultReceiver(); + service.isPlaying(broadcastId, mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return defaultValue; } /** @@ -402,7 +681,24 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public @NonNull List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() { - return Collections.emptyList(); + final IBluetoothLeAudio service = getService(); + final List<BluetoothLeBroadcastMetadata> defaultValue = Collections.emptyList(); + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<List<BluetoothLeBroadcastMetadata>> recv = + new SynchronousResultReceiver(); + service.getAllBroadcastMetadata(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return defaultValue; } /** @@ -413,7 +709,23 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getMaximumNumberOfBroadcast() { - return 1; + final IBluetoothLeAudio service = getService(); + final int defaultValue = 1; + if (service == null) { + Log.w(TAG, "Proxy not attached to service"); + if (DBG) log(Log.getStackTraceString(new Throwable())); + } else if (isEnabled()) { + try { + final SynchronousResultReceiver<Integer> recv = new SynchronousResultReceiver(); + service.getMaximumNumberOfBroadcast(mAttributionSource, recv); + return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue); + } catch (TimeoutException e) { + Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return defaultValue; } /** @@ -421,7 +733,29 @@ public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfi * @hide */ @Override - public void close() throws Exception {} + public void close() { + if (VDBG) log("close()"); + + IBluetoothManager mgr = mAdapter.getBluetoothManager(); + if (mgr != null) { + try { + mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + } + + mProfileConnector.disconnect(); + } + + private boolean isEnabled() { + if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; + return false; + } + + private IBluetoothLeAudio getService() { + return mProfileConnector.getService(); + } private static void log(String msg) { Log.d(TAG, msg); diff --git a/system/binder/Android.bp b/system/binder/Android.bp index 805f68c8cd..ff18409fd4 100644 --- a/system/binder/Android.bp +++ b/system/binder/Android.bp @@ -30,6 +30,7 @@ filegroup { "android/bluetooth/IBluetoothVolumeControl.aidl", "android/bluetooth/IBluetoothHidHost.aidl", "android/bluetooth/IBluetoothLeAudio.aidl", + "android/bluetooth/IBluetoothLeBroadcastCallback.aidl", "android/bluetooth/IBluetoothManager.aidl", "android/bluetooth/IBluetoothManagerCallback.aidl", "android/bluetooth/IBluetoothMap.aidl", diff --git a/system/binder/android/bluetooth/BluetoothLeAudioContentMetadata.aidl b/system/binder/android/bluetooth/BluetoothLeAudioContentMetadata.aidl new file mode 100644 index 0000000000..8962810c62 --- /dev/null +++ b/system/binder/android/bluetooth/BluetoothLeAudioContentMetadata.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +parcelable BluetoothLeAudioContentMetadata; diff --git a/system/binder/android/bluetooth/IBluetoothLeAudio.aidl b/system/binder/android/bluetooth/IBluetoothLeAudio.aidl index 337b9babd8..22a90a0758 100644 --- a/system/binder/android/bluetooth/IBluetoothLeAudio.aidl +++ b/system/binder/android/bluetooth/IBluetoothLeAudio.aidl @@ -18,6 +18,8 @@ package android.bluetooth; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothLeAudioContentMetadata; +import android.bluetooth.IBluetoothLeBroadcastCallback; import android.content.AttributionSource; import com.android.modules.utils.SynchronousResultReceiver; @@ -73,36 +75,20 @@ oneway interface IBluetoothLeAudio { void groupRemoveNode(int group_id, in BluetoothDevice device, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); // Broadcaster API - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void createBroadcast(in byte[] metadata, int audio_profile, in byte[] broadcast_code, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void updateMetadata(int instance_id, in byte[] metadata, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void startBroadcast(int instance_id, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void stopBroadcast(int instance_id, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void pauseBroadcast(int instance_id, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void destroyBroadcast(int instance_id, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void getBroadcastId(int instance_id, in AttributionSource attributionSource); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)") - void getAllBroadcastStates(in AttributionSource attributionSource); - - const int BROADCAST_INSTANCE_ID_UNDEFINED = 0xFF; - - const int BROADCAST_STATE_STOPPED = 0; - const int BROADCAST_STATE_CONFIGURING = 1; - const int BROADCAST_STATE_CONFIGURED = 2; - const int BROADCAST_STATE_STOPPING = 3; - const int BROADCAST_STATE_STREAMING = 4; - - const int BROADCASTER_ADDR_TYPE_PUBLIC = 0x00; - const int BROADCASTER_ADDR_TYPE_RANDOM = 0x01; - - const int BROADCAST_PROFILE_SONIFICATION = 0x00; - const int BROADCAST_PROFILE_MEDIA = 0x01; - - const int BIS_SYNC_ANY = 0xFFFFFFFF; + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void registerLeBroadcastCallback(in IBluetoothLeBroadcastCallback callback, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void unregisterLeBroadcastCallback(in IBluetoothLeBroadcastCallback callback, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void startBroadcast(in BluetoothLeAudioContentMetadata contentMetadata, in byte[] broadcastCode, in AttributionSource attributionSource); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void stopBroadcast(int broadcastId, in AttributionSource attributionSource); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void updateBroadcast(int broadcastId, in BluetoothLeAudioContentMetadata contentMetadata, in AttributionSource attributionSource); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT})") + void isPlaying(int broadcastId, in AttributionSource attributionSource, in SynchronousResultReceiver receiver); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT,android.Manifest.permission.BLUETOOTH_PRIVILEGED})") + void getAllBroadcastMetadata(in AttributionSource attributionSource, in SynchronousResultReceiver receiver); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(allOf={android.Manifest.permission.BLUETOOTH_CONNECT})") + void getMaximumNumberOfBroadcast(in AttributionSource attributionSource, in SynchronousResultReceiver receiver); } diff --git a/system/binder/android/bluetooth/IBluetoothLeBroadcastCallback.aidl b/system/binder/android/bluetooth/IBluetoothLeBroadcastCallback.aidl new file mode 100644 index 0000000000..d4df7c8dd5 --- /dev/null +++ b/system/binder/android/bluetooth/IBluetoothLeBroadcastCallback.aidl @@ -0,0 +1,44 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import android.bluetooth.BluetoothLeBroadcastMetadata; + +/** + * Callback definitions for interacting with Le Audio Broadcaster + * @hide + */ +oneway interface IBluetoothLeBroadcastCallback { + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastStarted(in int reason, in int broadcastId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastStartFailed(in int reason); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastStopped(in int reason, in int broadcastId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastStopFailed(in int reason); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onPlaybackStarted(in int reason, in int broadcastId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onPlaybackStopped(in int reason, in int broadcastId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastUpdated(in int reason, in int broadcastId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastUpdateFailed(in int reason, in int broadcastId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)") + void onBroadcastMetadataChanged(in int broadcastId, in BluetoothLeBroadcastMetadata metadata); +} diff --git a/system/bta/include/bta_le_audio_broadcaster_api.h b/system/bta/include/bta_le_audio_broadcaster_api.h index 428409c8fa..33022ef19b 100644 --- a/system/bta/include/bta_le_audio_broadcaster_api.h +++ b/system/bta/include/bta_le_audio_broadcaster_api.h @@ -26,8 +26,6 @@ /* Interface class */ class LeAudioBroadcaster { public: - typedef std::array<uint8_t, 16> Code; - enum class AudioProfile { SONIFICATION = 0, MEDIA = 1, @@ -47,10 +45,10 @@ class LeAudioBroadcaster { static bool IsLeAudioBroadcasterRunning(void); static void DebugDump(int fd); - virtual void CreateAudioBroadcast(std::vector<uint8_t> metadata, - AudioProfile profile, - std::optional<LeAudioBroadcaster::Code> - broadcast_code = std::nullopt) = 0; + virtual void CreateAudioBroadcast( + std::vector<uint8_t> metadata, AudioProfile profile, + std::optional<bluetooth::le_audio::BroadcastCode> broadcast_code = + std::nullopt) = 0; virtual void SuspendAudioBroadcast(uint8_t instance_id) = 0; virtual void StartAudioBroadcast(uint8_t instance_id) = 0; virtual void StopAudioBroadcast(uint8_t instance_id) = 0; diff --git a/system/bta/le_audio/broadcaster/broadcaster.cc b/system/bta/le_audio/broadcaster/broadcaster.cc index 2744fe9fc9..43460e4ecb 100644 --- a/system/bta/le_audio/broadcaster/broadcaster.cc +++ b/system/bta/le_audio/broadcaster/broadcaster.cc @@ -172,9 +172,10 @@ class LeAudioBroadcasterImpl : public LeAudioBroadcaster, public BigCallbacks { std::move(announcement)); } - void CreateAudioBroadcast( - std::vector<uint8_t> metadata, LeAudioBroadcaster::AudioProfile profile, - std::optional<LeAudioBroadcaster::Code> broadcast_code) override { + void CreateAudioBroadcast(std::vector<uint8_t> metadata, + LeAudioBroadcaster::AudioProfile profile, + std::optional<bluetooth::le_audio::BroadcastCode> + broadcast_code) override { DLOG(INFO) << __func__; auto& codec_wrapper = @@ -686,7 +687,7 @@ LeAudioBroadcasterImpl::LeAudioClientAudioSinkReceiverImpl void LeAudioBroadcaster::Initialize( bluetooth::le_audio::LeAudioBroadcasterCallbacks* callbacks, - base::Callback<bool()> hal_2_1_verifier) { + base::Callback<bool()> audio_hal_verifier) { LOG(INFO) << "Broadcaster " << __func__; if (instance) { LOG(ERROR) << "Already initialized"; @@ -701,7 +702,7 @@ void LeAudioBroadcaster::Initialize( IsoManager::GetInstance()->Start(); - if (!std::move(hal_2_1_verifier).Run()) { + if (!std::move(audio_hal_verifier).Run()) { LOG_ASSERT(false) << __func__ << ", HAL 2.1 not supported, Init aborted."; return; } diff --git a/system/bta/le_audio/broadcaster/broadcaster_test.cc b/system/bta/le_audio/broadcaster/broadcaster_test.cc index e0466dd041..d028451105 100644 --- a/system/bta/le_audio/broadcaster/broadcaster_test.cc +++ b/system/bta/le_audio/broadcaster/broadcaster_test.cc @@ -64,7 +64,7 @@ namespace broadcaster { namespace { static constexpr LeAudioBroadcaster::AudioProfile default_profile = LeAudioBroadcaster::AudioProfile::SONIFICATION; -static constexpr LeAudioBroadcaster::Code default_code = { +static constexpr BroadcastCode default_code = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10}; static const std::vector<uint8_t> default_metadata = {0x03, 0x02, 0x01, 0x00}; @@ -143,7 +143,7 @@ class BroadcasterTest : public Test { uint8_t InstantiateBroadcast( LeAudioBroadcaster::AudioProfile profile = default_profile, std::vector<uint8_t> metadata = default_metadata, - LeAudioBroadcaster::Code code = default_code) { + BroadcastCode code = default_code) { uint8_t instance_id = LeAudioBroadcaster::kInstanceIdUndefined; EXPECT_CALL(mock_broadcaster_callbacks_, OnBroadcastCreated(_, true)) .WillOnce(SaveArg<0>(&instance_id)); diff --git a/system/bta/le_audio/broadcaster/mock_state_machine.h b/system/bta/le_audio/broadcaster/mock_state_machine.h index 738dd5d0df..69480ab2d7 100644 --- a/system/bta/le_audio/broadcaster/mock_state_machine.h +++ b/system/bta/le_audio/broadcaster/mock_state_machine.h @@ -97,7 +97,9 @@ class MockBroadcastStateMachine MOCK_METHOD((void), RequestOwnAddress, (), (override)); MOCK_METHOD((RawAddress), GetOwnAddress, (), (override)); MOCK_METHOD((uint8_t), GetOwnAddressType, (), (override)); - MOCK_METHOD((bluetooth::le_audio::BroadcastId), GetBroadcastId, (), + MOCK_METHOD((std::optional<bluetooth::le_audio::BroadcastCode>), + GetBroadcastCode, (), (const override)); + MOCK_METHOD((bluetooth::le_audio::BroadcastId const&), GetBroadcastId, (), (const override)); MOCK_METHOD((void), UpdateBroadcastAnnouncement, (le_audio::broadcaster::BasicAudioAnnouncementData announcement), diff --git a/system/bta/le_audio/broadcaster/state_machine.cc b/system/bta/le_audio/broadcaster/state_machine.cc index be69818e48..8b7b035626 100644 --- a/system/bta/le_audio/broadcaster/state_machine.cc +++ b/system/bta/le_audio/broadcaster/state_machine.cc @@ -101,10 +101,15 @@ class BroadcastStateMachineImpl : public BroadcastStateMachine { return addr_type_; } - bluetooth::le_audio::BroadcastId GetBroadcastId() const override { + bluetooth::le_audio::BroadcastId const& GetBroadcastId() const override { return sm_config_.broadcast_id; } + std::optional<bluetooth::le_audio::BroadcastCode> GetBroadcastCode() + const override { + return sm_config_.broadcast_code; + } + void UpdateBroadcastAnnouncement( BasicAudioAnnouncementData announcement) override { std::vector<uint8_t> periodic_data; diff --git a/system/bta/le_audio/broadcaster/state_machine.h b/system/bta/le_audio/broadcaster/state_machine.h index 45d73c6b8d..5877a8c445 100644 --- a/system/bta/le_audio/broadcaster/state_machine.h +++ b/system/bta/le_audio/broadcaster/state_machine.h @@ -98,7 +98,7 @@ struct BroadcastStateMachineConfig { uint8_t streaming_phy; BroadcastCodecWrapper codec_wrapper; BasicAudioAnnouncementData announcement; - std::optional<LeAudioBroadcaster::Code> broadcast_code; + std::optional<bluetooth::le_audio::BroadcastCode> broadcast_code; }; class BroadcastStateMachine : public StateMachine<5> { @@ -144,7 +144,9 @@ class BroadcastStateMachine : public StateMachine<5> { virtual void RequestOwnAddress() = 0; virtual RawAddress GetOwnAddress() = 0; virtual uint8_t GetOwnAddressType() = 0; - virtual bluetooth::le_audio::BroadcastId GetBroadcastId() const = 0; + virtual std::optional<bluetooth::le_audio::BroadcastCode> GetBroadcastCode() + const = 0; + virtual bluetooth::le_audio::BroadcastId const& GetBroadcastId() const = 0; virtual void UpdateBroadcastAnnouncement( BasicAudioAnnouncementData announcement) = 0; void SetMuted(bool muted) { is_muted_ = muted; }; diff --git a/system/bta/le_audio/broadcaster/state_machine_test.cc b/system/bta/le_audio/broadcaster/state_machine_test.cc index a203bcab2b..f6cf786e58 100644 --- a/system/bta/le_audio/broadcaster/state_machine_test.cc +++ b/system/bta/le_audio/broadcaster/state_machine_test.cc @@ -218,11 +218,14 @@ class StateMachineTest : public Test { std::future<uint8_t> instance_future = instance_creation_promise_.get_future(); + static uint8_t broadcast_id_lsb = 1; + auto codec_wrapper = BroadcastCodecWrapper::getCodecConfigForProfile(profile); pending_broadcasts_.push_back(BroadcastStateMachine::CreateInstance({ - .codec_wrapper = std::move(codec_wrapper), + .broadcast_id = {0, 0, broadcast_id_lsb++}, // .streaming_phy = , + .codec_wrapper = std::move(codec_wrapper), // .announcement = , // .broadcast_code = , })); @@ -753,6 +756,19 @@ TEST_F(StateMachineTest, GetConfig) { ASSERT_EQ(big_cfg->connection_handles.size(), num_channels); } +TEST_F(StateMachineTest, GetBroadcastId) { + EXPECT_CALL(*(sm_callbacks_.get()), OnStateMachineCreateStatus(_, true)) + .Times(1); + + uint8_t instance_id = InstantiateStateMachine(); + ASSERT_EQ(broadcasts_[instance_id]->GetState(), + BroadcastStateMachine::State::CONFIGURED); + + bluetooth::le_audio::BroadcastId const& broadcast_id = + broadcasts_[instance_id]->GetBroadcastId(); + ASSERT_NE(bluetooth::le_audio::kBroadcastBroadcastIdInvalid, broadcast_id); +} + TEST_F(StateMachineTest, AnnouncementUUIDs) { std::vector<uint8_t> a_data; std::vector<uint8_t> p_data; diff --git a/system/include/hardware/bt_le_audio.h b/system/include/hardware/bt_le_audio.h index c5cd87073a..44089b81b0 100644 --- a/system/include/hardware/bt_le_audio.h +++ b/system/include/hardware/bt_le_audio.h @@ -144,9 +144,10 @@ enum class BroadcastAudioProfile { MEDIA, }; -using BroadcastCode = std::array<uint8_t, 16>; -using BroadcastId = std::array<uint8_t, 3>; constexpr uint8_t kBroadcastAnnouncementBroadcastIdSize = 3; +using BroadcastId = std::array<uint8_t, kBroadcastAnnouncementBroadcastIdSize>; +constexpr BroadcastId kBroadcastBroadcastIdInvalid = {0, 0, 0}; +using BroadcastCode = std::array<uint8_t, 16>; class LeAudioBroadcasterCallbacks { public: |