/* * Copyright 2021 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.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 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.Objects; import java.util.concurrent.Executor; /** * This class provides the public APIs to control the BAP Broadcast Source profile. * *

BluetoothLeBroadcast is a proxy object for controlling the Bluetooth LE Broadcast Source * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothLeBroadcast * proxy object. * * @hide */ @SystemApi 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 IBluetoothLeAudio mService; private final Map mCallbackExecutorMap = new HashMap<>(); @SuppressLint("AndroidFrameworkBluetoothPermission") private final IBluetoothLeBroadcastCallback mCallback = new IBluetoothLeBroadcastCallback.Stub() { @Override public void onBroadcastStarted(int reason, int broadcastId) { for (Map.Entry 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 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 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 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 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 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 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 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 callbackExecutorEntry : mCallbackExecutorMap.entrySet()) { BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey(); Executor executor = callbackExecutorEntry.getValue(); executor.execute( () -> callback.onBroadcastMetadataChanged(broadcastId, metadata)); } } }; /** * Interface for receiving events related to Broadcast Source * * @hide */ @SystemApi public interface Callback { /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( value = { BluetoothStatusCodes.ERROR_UNKNOWN, BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST, BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST, BluetoothStatusCodes.REASON_SYSTEM_POLICY, BluetoothStatusCodes.ERROR_HARDWARE_GENERIC, BluetoothStatusCodes.ERROR_BAD_PARAMETERS, BluetoothStatusCodes.ERROR_LOCAL_NOT_ENOUGH_RESOURCES, BluetoothStatusCodes.ERROR_LE_BROADCAST_INVALID_CODE, BluetoothStatusCodes.ERROR_LE_BROADCAST_INVALID_BROADCAST_ID, BluetoothStatusCodes.ERROR_LE_CONTENT_METADATA_INVALID_PROGRAM_INFO, BluetoothStatusCodes.ERROR_LE_CONTENT_METADATA_INVALID_LANGUAGE, BluetoothStatusCodes.ERROR_LE_CONTENT_METADATA_INVALID_OTHER, }) @interface Reason {} /** * Callback invoked when broadcast is started, but audio may not be playing. * *

Caller should wait for {@link #onBroadcastMetadataChanged(int, * BluetoothLeBroadcastMetadata)} for the updated metadata * * @param reason for broadcast start * @param broadcastId as defined by the Basic Audio Profile * @hide */ @SystemApi void onBroadcastStarted(@Reason int reason, int broadcastId); /** * Callback invoked when broadcast failed to start * * @param reason for broadcast start failure * @hide */ @SystemApi void onBroadcastStartFailed(@Reason int reason); /** * Callback invoked when broadcast is stopped * * @param reason for broadcast stop * @hide */ @SystemApi void onBroadcastStopped(@Reason int reason, int broadcastId); /** * Callback invoked when broadcast failed to stop * * @param reason for broadcast stop failure * @hide */ @SystemApi void onBroadcastStopFailed(@Reason int reason); /** * Callback invoked when broadcast audio is playing * * @param reason for playback start * @param broadcastId as defined by the Basic Audio Profile * @hide */ @SystemApi void onPlaybackStarted(@Reason int reason, int broadcastId); /** * Callback invoked when broadcast audio is not playing * * @param reason for playback stop * @param broadcastId as defined by the Basic Audio Profile * @hide */ @SystemApi void onPlaybackStopped(@Reason int reason, int broadcastId); /** * Callback invoked when encryption is enabled * * @param reason for encryption enable * @param broadcastId as defined by the Basic Audio Profile * @hide */ @SystemApi void onBroadcastUpdated(@Reason int reason, int broadcastId); /** * Callback invoked when Broadcast Source failed to update * * @param reason for update failure * @param broadcastId as defined by the Basic Audio Profile * @hide */ @SystemApi void onBroadcastUpdateFailed(int reason, int broadcastId); /** * Callback invoked when Broadcast Source metadata is updated * * @param metadata updated Broadcast Source metadata * @param broadcastId as defined by the Basic Audio Profile * @hide */ @SystemApi void onBroadcastMetadataChanged( int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata); } /** * Create a BluetoothLeBroadcast proxy object for interacting with the local LE Audio Broadcast * Source service. * * @param context for to operate this API class * @hide */ /*package*/ BluetoothLeBroadcast(Context context, BluetoothAdapter adapter) { mAdapter = adapter; mAttributionSource = mAdapter.getAttributionSource(); mService = null; mCloseGuard = new CloseGuard(); mCloseGuard.open("close"); } /** @hide */ @SuppressWarnings("Finalize") // TODO(b/314811467) protected void finalize() { if (mCloseGuard != null) { mCloseGuard.warnIfOpen(); } close(); } /** * Not supported since LE Audio Broadcasts do not establish a connection. * * @hide */ @Override @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public int getConnectionState(@NonNull BluetoothDevice device) { throw new UnsupportedOperationException("LE Audio Broadcasts are not connection-oriented."); } /** * Not supported since LE Audio Broadcasts do not establish a connection. * * @hide */ @Override @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) @NonNull public List getDevicesMatchingConnectionStates(@NonNull int[] states) { throw new UnsupportedOperationException("LE Audio Broadcasts are not connection-oriented."); } /** * Not supported since LE Audio Broadcasts do not establish a connection. * * @hide */ @Override @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public @NonNull List getConnectedDevices() { throw new UnsupportedOperationException("LE Audio Broadcasts are not connection-oriented."); } /** * Register a {@link Callback} that will be invoked during the operation of this profile. * *

Repeated registration of the same callback object after the first call to this * method will result with IllegalArgumentException being thrown, even when the * executor is different. API caller would have to call {@link * #unregisterCallback(Callback)} with the same callback object before registering it again. * * @param executor an {@link Executor} to execute given callback * @param callback user implementation of the {@link Callback} * @throws NullPointerException if a null executor, or callback is given, or * IllegalArgumentException if the same callback is already registered. * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void registerCallback( @NonNull @CallbackExecutor Executor executor, @NonNull Callback callback) { Objects.requireNonNull(executor, "executor cannot be null"); Objects.requireNonNull(callback, "callback cannot be null"); if (DBG) log("registerCallback"); synchronized (mCallbackExecutorMap) { // If the callback map is empty, we register the service-to-app callback if (mCallbackExecutorMap.isEmpty()) { if (!mAdapter.isEnabled()) { /* If Bluetooth is off, just store callback and it will be registered * when Bluetooth is on */ mCallbackExecutorMap.put(callback, executor); return; } try { final IBluetoothLeAudio service = getService(); if (service != null) { service.registerLeBroadcastCallback(mCallback, mAttributionSource); } } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } // 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); } } /** * Unregister the specified {@link Callback} * *

The same {@link Callback} object used when calling {@link #registerCallback(Executor, * Callback)} must be used. * *

Callbacks are automatically unregistered when application process goes away * * @param callback user implementation of the {@link Callback} * @throws NullPointerException when callback is null or IllegalArgumentException when no * callback is registered * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void unregisterCallback(@NonNull Callback callback) { Objects.requireNonNull(callback, "callback cannot be null"); 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) { service.unregisterLeBroadcastCallback(mCallback, mAttributionSource); } } catch (IllegalStateException e) { Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable())); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } /** * Start broadcasting to nearby devices using broadcastCode and * contentMetadata * *

Encryption will be enabled when broadcastCode is not null. * *

As defined in Volume 3, Part C, Section 3.2.6 of Bluetooth Core Specification, Version * 5.3, Broadcast Code is used to encrypt a broadcast audio stream. * *

It must be a UTF-8 string that has at least 4 octets and should not exceed 16 octets. * *

If the provided broadcastCode is non-null and does not meet the above * requirements, encryption will fail to enable with reason code {@link * BluetoothStatusCodes#ERROR_LE_BROADCAST_INVALID_CODE} * *

Caller can set content metadata such as program information string in * contentMetadata * *

On success, {@link Callback#onBroadcastStarted(int, int)} will be invoked with {@link * BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} reason code. On failure, {@link * Callback#onBroadcastStartFailed(int)} will be invoked with reason code. * *

In particular, when the number of Broadcast Sources reaches {@link * #getMaximumNumberOfBroadcast()}, this method will fail with {@link * BluetoothStatusCodes#ERROR_LOCAL_NOT_ENOUGH_RESOURCES} * *

After broadcast is started, {@link Callback#onBroadcastMetadataChanged(int, * BluetoothLeBroadcastMetadata)} will be invoked to expose the latest Broadcast Group metadata * that can be shared out of band to set up Broadcast Sink without scanning. * *

Alternatively, one can also get the latest Broadcast Source meta via {@link * #getAllBroadcastMetadata()} * * @param contentMetadata metadata for the default Broadcast subgroup * @param broadcastCode Encryption will be enabled when broadcastCode is not null * @throws IllegalStateException if callback was not registered * @throws NullPointerException if contentMetadata is null * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void startBroadcast( @NonNull BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode) { Objects.requireNonNull(contentMetadata, "contentMetadata cannot be null"); if (mCallbackExecutorMap.isEmpty()) { throw new IllegalStateException("No callback was ever registered"); } 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( buildBroadcastSettingsFromMetadata(contentMetadata, broadcastCode), mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } /** * Start broadcasting to nearby devices using {@link BluetoothLeBroadcastSettings}. * * @param broadcastSettings broadcast settings for this broadcast group * @throws IllegalStateException if callback was not registered * @throws NullPointerException if broadcastSettings is null * @hide */ @SystemApi @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void startBroadcast(@NonNull BluetoothLeBroadcastSettings broadcastSettings) { Objects.requireNonNull(broadcastSettings, "broadcastSettings cannot be null"); if (mCallbackExecutorMap.isEmpty()) { throw new IllegalStateException("No callback was ever registered"); } 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(broadcastSettings, mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } /** * Update the broadcast with broadcastId with new contentMetadata * *

On success, {@link Callback#onBroadcastUpdated(int, int)} will be invoked with reason code * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure, {@link * Callback#onBroadcastUpdateFailed(int, int)} will be invoked with reason code * * @param broadcastId broadcastId as defined by the Basic Audio Profile * @param contentMetadata updated metadata for the default Broadcast subgroup * @throws IllegalStateException if callback was not registered * @throws NullPointerException if contentMetadata is null * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void updateBroadcast( int broadcastId, @NonNull BluetoothLeAudioContentMetadata contentMetadata) { Objects.requireNonNull(contentMetadata, "contentMetadata cannot be null"); if (mCallbackExecutorMap.isEmpty()) { throw new IllegalStateException("No callback was ever registered"); } 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, buildBroadcastSettingsFromMetadata(contentMetadata, null), mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } /** * Update the broadcast with broadcastId with BluetoothLeBroadcastSettings * *

On success, {@link Callback#onBroadcastUpdated(int, int)} will be invoked with reason code * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure, {@link * Callback#onBroadcastUpdateFailed(int, int)} will be invoked with reason code * * @param broadcastId broadcastId as defined by the Basic Audio Profile * @param broadcastSettings broadcast settings for this broadcast group * @throws IllegalStateException if callback was not registered * @throws NullPointerException if broadcastSettings is null * @hide */ @SystemApi @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void updateBroadcast( int broadcastId, @NonNull BluetoothLeBroadcastSettings broadcastSettings) { Objects.requireNonNull(broadcastSettings, "broadcastSettings cannot be null"); if (mCallbackExecutorMap.isEmpty()) { throw new IllegalStateException("No callback was ever registered"); } 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, broadcastSettings, mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } } /** * Stop broadcasting. * *

On success, {@link Callback#onBroadcastStopped(int, int)} will be invoked with reason code * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} and the broadcastId On * failure, {@link Callback#onBroadcastStopFailed(int)} will be invoked with reason code * * @param broadcastId as defined by the Basic Audio Profile * @throws IllegalStateException if callback was not registered * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public void stopBroadcast(int broadcastId) { if (mCallbackExecutorMap.isEmpty()) { throw new IllegalStateException("No callback was ever registered"); } 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.rethrowAsRuntimeException(); } } } /** * Return true if audio is being broadcasted on the Broadcast Source as identified by the * broadcastId * * @param broadcastId as defined in the Basic Audio Profile * @return true if audio is being broadcasted * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public boolean isPlaying(int broadcastId) { 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 { return service.isPlaying(broadcastId, mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } return false; } /** * Get {@link BluetoothLeBroadcastMetadata} for all Broadcast Groups currently running on this * device * * @return list of {@link BluetoothLeBroadcastMetadata} * @hide */ @SystemApi @RequiresBluetoothConnectPermission @RequiresPermission( allOf = { android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_PRIVILEGED, }) public @NonNull List getAllBroadcastMetadata() { 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 { return service.getAllBroadcastMetadata(mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } return Collections.emptyList(); } /** * Get the maximum number of Broadcast Isochronous Group supported on this device * * @return maximum number of Broadcast Isochronous Group supported on this device * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getMaximumNumberOfBroadcasts() { 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 { return service.getMaximumNumberOfBroadcasts(mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } return 1; } /** * Get the maximum number of streams per broadcast Single stream means single Audio PCM stream * * @return maximum number of broadcast streams per broadcast group * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getMaximumStreamsPerBroadcast() { 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 { return service.getMaximumStreamsPerBroadcast(mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } return 1; } /** * Get the maximum number of subgroups per broadcast Single stream means single Audio PCM * stream, one stream could support single or multiple subgroups based on language and audio * configuration. e.g. Stream 1 -> 2 subgroups with English and Spanish, Stream 2 -> 1 subgroups * with English, Stream 3 -> 2 subgroups with hearing Aids Standard and High Quality * * @return maximum number of broadcast subgroups per broadcast group * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) public int getMaximumSubgroupsPerBroadcast() { 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 { return service.getMaximumSubgroupsPerBroadcast(mAttributionSource); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } } return 1; } /** * {@inheritDoc} * * @hide */ @Override public void close() { if (VDBG) log("close()"); mAdapter.closeProfileProxy(this); } private BluetoothLeBroadcastSettings buildBroadcastSettingsFromMetadata( BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode) { BluetoothLeBroadcastSubgroupSettings.Builder subgroupBuilder = new BluetoothLeBroadcastSubgroupSettings.Builder() .setContentMetadata(contentMetadata); BluetoothLeBroadcastSettings.Builder builder = new BluetoothLeBroadcastSettings.Builder() .setPublicBroadcast(false) .setBroadcastCode(broadcastCode); // builder expect at least one subgroup setting builder.addSubgroupSettings(subgroupBuilder.build()); return builder.build(); } private boolean isEnabled() { if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; return false; } /** @hide */ @Override public void onServiceConnected(IBinder service) { mService = IBluetoothLeAudio.Stub.asInterface(service); // re-register the service-to-app callback synchronized (mCallbackExecutorMap) { if (mCallbackExecutorMap.isEmpty()) { return; } try { if (service != null) { mService.registerLeBroadcastCallback(mCallback, mAttributionSource); } } catch (RemoteException e) { Log.e( TAG, "onServiceConnected: Failed to register " + "Le Broadcaster callback", e); } } } /** @hide */ @Override public void onServiceDisconnected() { mService = null; } private IBluetoothLeAudio getService() { return mService; } /** @hide */ @Override public BluetoothAdapter getAdapter() { return mAdapter; } private static void log(String msg) { Log.d(TAG, msg); } }