• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at:
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.bluetooth;
18 
19 import static android.bluetooth.BluetoothUtils.getSyncTimeout;
20 
21 import android.annotation.CallbackExecutor;
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.RequiresPermission;
26 import android.annotation.SuppressLint;
27 import android.annotation.SystemApi;
28 import android.bluetooth.annotations.RequiresBluetoothConnectPermission;
29 import android.content.AttributionSource;
30 import android.content.Context;
31 import android.os.IBinder;
32 import android.os.RemoteException;
33 import android.util.CloseGuard;
34 import android.util.Log;
35 
36 import com.android.modules.utils.SynchronousResultReceiver;
37 
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.Collections;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.TimeoutException;
47 
48 /**
49  * This class provides the public APIs to control the BAP Broadcast Source profile.
50  *
51  * <p>BluetoothLeBroadcast is a proxy object for controlling the Bluetooth LE Broadcast Source
52  * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get the BluetoothLeBroadcast
53  * proxy object.
54  *
55  * @hide
56  */
57 @SystemApi
58 public final class BluetoothLeBroadcast implements AutoCloseable, BluetoothProfile {
59     private static final String TAG = "BluetoothLeBroadcast";
60     private static final boolean DBG = true;
61     private static final boolean VDBG = false;
62 
63     private CloseGuard mCloseGuard;
64 
65     private final BluetoothAdapter mAdapter;
66     private final AttributionSource mAttributionSource;
67     private final BluetoothProfileConnector<IBluetoothLeAudio> mProfileConnector =
68             new BluetoothProfileConnector(this, BluetoothProfile.LE_AUDIO_BROADCAST,
69                     "BluetoothLeAudioBroadcast", IBluetoothLeAudio.class.getName()) {
70                 @Override
71                 public IBluetoothLeAudio getServiceInterface(IBinder service) {
72                     return IBluetoothLeAudio.Stub.asInterface(service);
73                 }
74             };
75 
76     private final Map<Callback, Executor> mCallbackExecutorMap = new HashMap<>();
77 
78     @SuppressLint("AndroidFrameworkBluetoothPermission")
79     private final IBluetoothLeBroadcastCallback mCallback =
80             new IBluetoothLeBroadcastCallback.Stub() {
81         @Override
82         public void onBroadcastStarted(int reason, int broadcastId) {
83             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
84                     mCallbackExecutorMap.entrySet()) {
85                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
86                 Executor executor = callbackExecutorEntry.getValue();
87                 executor.execute(() -> callback.onBroadcastStarted(reason, broadcastId));
88             }
89         }
90 
91         @Override
92         public void onBroadcastStartFailed(int reason) {
93             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
94                     mCallbackExecutorMap.entrySet()) {
95                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
96                 Executor executor = callbackExecutorEntry.getValue();
97                 executor.execute(() -> callback.onBroadcastStartFailed(reason));
98             }
99         }
100 
101         @Override
102         public void onBroadcastStopped(int reason, int broadcastId) {
103             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
104                     mCallbackExecutorMap.entrySet()) {
105                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
106                 Executor executor = callbackExecutorEntry.getValue();
107                 executor.execute(() -> callback.onBroadcastStopped(reason, broadcastId));
108             }
109         }
110 
111         @Override
112         public void onBroadcastStopFailed(int reason) {
113             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
114                     mCallbackExecutorMap.entrySet()) {
115                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
116                 Executor executor = callbackExecutorEntry.getValue();
117                 executor.execute(() -> callback.onBroadcastStopFailed(reason));
118             }
119         }
120 
121         @Override
122         public void onPlaybackStarted(int reason, int broadcastId) {
123             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
124                     mCallbackExecutorMap.entrySet()) {
125                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
126                 Executor executor = callbackExecutorEntry.getValue();
127                 executor.execute(() -> callback.onPlaybackStarted(reason, broadcastId));
128             }
129         }
130 
131         @Override
132         public void onPlaybackStopped(int reason, int broadcastId) {
133             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
134                     mCallbackExecutorMap.entrySet()) {
135                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
136                 Executor executor = callbackExecutorEntry.getValue();
137                 executor.execute(() -> callback.onPlaybackStopped(reason, broadcastId));
138             }
139         }
140 
141         @Override
142         public void onBroadcastUpdated(int reason, int broadcastId) {
143             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
144                     mCallbackExecutorMap.entrySet()) {
145                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
146                 Executor executor = callbackExecutorEntry.getValue();
147                 executor.execute(() -> callback.onBroadcastUpdated(reason, broadcastId));
148             }
149         }
150 
151         @Override
152         public void onBroadcastUpdateFailed(int reason, int broadcastId) {
153             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
154                     mCallbackExecutorMap.entrySet()) {
155                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
156                 Executor executor = callbackExecutorEntry.getValue();
157                 executor.execute(() -> callback.onBroadcastUpdateFailed(reason, broadcastId));
158             }
159         }
160 
161         @Override
162         public void onBroadcastMetadataChanged(int broadcastId,
163                 BluetoothLeBroadcastMetadata metadata) {
164             for (Map.Entry<BluetoothLeBroadcast.Callback, Executor> callbackExecutorEntry:
165                     mCallbackExecutorMap.entrySet()) {
166                 BluetoothLeBroadcast.Callback callback = callbackExecutorEntry.getKey();
167                 Executor executor = callbackExecutorEntry.getValue();
168                 executor.execute(() -> callback.onBroadcastMetadataChanged(broadcastId, metadata));
169             }
170         }
171     };
172 
173     private final class BluetoothLeBroadcastServiceListener extends ForwardingServiceListener {
BluetoothLeBroadcastServiceListener(ServiceListener listener)174         BluetoothLeBroadcastServiceListener(ServiceListener listener) {
175             super(listener);
176         }
177 
178         @Override
onServiceConnected(int profile, BluetoothProfile proxy)179         public void onServiceConnected(int profile, BluetoothProfile proxy) {
180             try {
181                 if (profile == LE_AUDIO_BROADCAST) {
182                     // re-register the service-to-app callback
183                     synchronized (mCallbackExecutorMap) {
184                         if (mCallbackExecutorMap.isEmpty()) {
185                             return;
186                         }
187                         try {
188                             final IBluetoothLeAudio service = getService();
189                             if (service != null) {
190                                 final SynchronousResultReceiver<Integer> recv =
191                                         SynchronousResultReceiver.get();
192                                 service.registerLeBroadcastCallback(mCallback,
193                                         mAttributionSource, recv);
194                                 recv.awaitResultNoInterrupt(getSyncTimeout())
195                                         .getValue(null);
196                             }
197                         } catch (TimeoutException e) {
198                             Log.e(TAG, "onBluetoothServiceUp: Failed to register "
199                                     + "Le Broadcaster callback", e);
200                         } catch (RemoteException e) {
201                             throw e.rethrowFromSystemServer();
202                         }
203                     }
204                 }
205             } finally {
206                 super.onServiceConnected(profile, proxy);
207             }
208         }
209     }
210 
211     /**
212      * Interface for receiving events related to Broadcast Source
213      * @hide
214      */
215     @SystemApi
216     public interface Callback {
217         /** @hide */
218         @Retention(RetentionPolicy.SOURCE)
219         @IntDef(value = {
220                 BluetoothStatusCodes.ERROR_UNKNOWN,
221                 BluetoothStatusCodes.REASON_LOCAL_APP_REQUEST,
222                 BluetoothStatusCodes.REASON_LOCAL_STACK_REQUEST,
223                 BluetoothStatusCodes.REASON_SYSTEM_POLICY,
224                 BluetoothStatusCodes.ERROR_HARDWARE_GENERIC,
225                 BluetoothStatusCodes.ERROR_BAD_PARAMETERS,
226                 BluetoothStatusCodes.ERROR_LOCAL_NOT_ENOUGH_RESOURCES,
227                 BluetoothStatusCodes.ERROR_LE_BROADCAST_INVALID_CODE,
228                 BluetoothStatusCodes.ERROR_LE_BROADCAST_INVALID_BROADCAST_ID,
229                 BluetoothStatusCodes.ERROR_LE_CONTENT_METADATA_INVALID_PROGRAM_INFO,
230                 BluetoothStatusCodes.ERROR_LE_CONTENT_METADATA_INVALID_LANGUAGE,
231                 BluetoothStatusCodes.ERROR_LE_CONTENT_METADATA_INVALID_OTHER,
232         })
233         @interface Reason {}
234 
235         /**
236          * Callback invoked when broadcast is started, but audio may not be playing.
237          *
238          * Caller should wait for
239          * {@link #onBroadcastMetadataChanged(int, BluetoothLeBroadcastMetadata)}
240          * for the updated metadata
241          *
242          * @param reason for broadcast start
243          * @param broadcastId as defined by the Basic Audio Profile
244          * @hide
245          */
246         @SystemApi
onBroadcastStarted(@eason int reason, int broadcastId)247         void onBroadcastStarted(@Reason int reason, int broadcastId);
248 
249         /**
250          * Callback invoked when broadcast failed to start
251          *
252          * @param reason for broadcast start failure
253          * @hide
254          */
255         @SystemApi
onBroadcastStartFailed(@eason int reason)256         void onBroadcastStartFailed(@Reason int reason);
257 
258         /**
259          * Callback invoked when broadcast is stopped
260          *
261          * @param reason for broadcast stop
262          * @hide
263          */
264         @SystemApi
onBroadcastStopped(@eason int reason, int broadcastId)265         void onBroadcastStopped(@Reason int reason, int broadcastId);
266 
267         /**
268          * Callback invoked when broadcast failed to stop
269          *
270          * @param reason for broadcast stop failure
271          * @hide
272          */
273         @SystemApi
onBroadcastStopFailed(@eason int reason)274         void onBroadcastStopFailed(@Reason int reason);
275 
276         /**
277          * Callback invoked when broadcast audio is playing
278          *
279          * @param reason for playback start
280          * @param broadcastId as defined by the Basic Audio Profile
281          * @hide
282          */
283         @SystemApi
onPlaybackStarted(@eason int reason, int broadcastId)284         void onPlaybackStarted(@Reason int reason, int broadcastId);
285 
286         /**
287          * Callback invoked when broadcast audio is not playing
288          *
289          * @param reason for playback stop
290          * @param broadcastId as defined by the Basic Audio Profile
291          * @hide
292          */
293         @SystemApi
onPlaybackStopped(@eason int reason, int broadcastId)294         void onPlaybackStopped(@Reason int reason, int broadcastId);
295 
296         /**
297          * Callback invoked when encryption is enabled
298          *
299          * @param reason for encryption enable
300          * @param broadcastId as defined by the Basic Audio Profile
301          * @hide
302          */
303         @SystemApi
onBroadcastUpdated(@eason int reason, int broadcastId)304         void onBroadcastUpdated(@Reason int reason, int broadcastId);
305 
306         /**
307          * Callback invoked when Broadcast Source failed to update
308          *
309          * @param reason for update failure
310          * @param broadcastId as defined by the Basic Audio Profile
311          * @hide
312          */
313         @SystemApi
onBroadcastUpdateFailed(int reason, int broadcastId)314         void onBroadcastUpdateFailed(int reason, int broadcastId);
315 
316         /**
317          * Callback invoked when Broadcast Source metadata is updated
318          *
319          * @param metadata updated Broadcast Source metadata
320          * @param broadcastId as defined by the Basic Audio Profile
321          * @hide
322          */
323         @SystemApi
onBroadcastMetadataChanged(int broadcastId, @NonNull BluetoothLeBroadcastMetadata metadata)324         void onBroadcastMetadataChanged(int broadcastId,
325                 @NonNull BluetoothLeBroadcastMetadata metadata);
326     }
327 
328     /**
329      * Create a BluetoothLeBroadcast proxy object for interacting with the local LE Audio Broadcast
330      * Source service.
331      *
332      * @param context  for to operate this API class
333      * @param listener listens for service callbacks across binder
334      * @hide
335      */
BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener)336     /*package*/ BluetoothLeBroadcast(Context context, BluetoothProfile.ServiceListener listener) {
337         mAdapter = BluetoothAdapter.getDefaultAdapter();
338         mAttributionSource = mAdapter.getAttributionSource();
339         mProfileConnector.connect(context, new BluetoothLeBroadcastServiceListener(listener));
340 
341         mCloseGuard = new CloseGuard();
342         mCloseGuard.open("close");
343     }
344 
345     /**
346      * @hide
347      */
finalize()348     protected void finalize() {
349         if (mCloseGuard != null) {
350             mCloseGuard.warnIfOpen();
351         }
352         close();
353     }
354 
355     /**
356      * Not supported since LE Audio Broadcasts do not establish a connection.
357      *
358      * @hide
359      */
360     @Override
361     @RequiresBluetoothConnectPermission
362     @RequiresPermission(allOf = {
363             android.Manifest.permission.BLUETOOTH_CONNECT,
364             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
365     })
getConnectionState(@onNull BluetoothDevice device)366     public int getConnectionState(@NonNull BluetoothDevice device) {
367         throw new UnsupportedOperationException("LE Audio Broadcasts are not connection-oriented.");
368     }
369 
370     /**
371      * Not supported since LE Audio Broadcasts do not establish a connection.
372      *
373      * @hide
374      */
375     @Override
376     @RequiresBluetoothConnectPermission
377     @RequiresPermission(allOf = {
378             android.Manifest.permission.BLUETOOTH_CONNECT,
379             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
380     })
381     @NonNull
getDevicesMatchingConnectionStates( @onNull int[] states)382     public List<BluetoothDevice> getDevicesMatchingConnectionStates(
383             @NonNull int[] states) {
384         throw new UnsupportedOperationException("LE Audio Broadcasts are not connection-oriented.");
385     }
386 
387     /**
388      * Not supported since LE Audio Broadcasts do not establish a connection.
389      *
390      * @hide
391      */
392     @Override
393     @RequiresBluetoothConnectPermission
394     @RequiresPermission(allOf = {
395             android.Manifest.permission.BLUETOOTH_CONNECT,
396             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
397     })
getConnectedDevices()398     public @NonNull List<BluetoothDevice> getConnectedDevices() {
399         throw new UnsupportedOperationException("LE Audio Broadcasts are not connection-oriented.");
400     }
401 
402     /**
403      * Register a {@link Callback} that will be invoked during the operation of this profile.
404      *
405      * Repeated registration of the same <var>callback</var> object after the first call to this
406      * method will result with IllegalArgumentException being thrown, even when the
407      * <var>executor</var> is different. API caller would have to call
408      * {@link #unregisterCallback(Callback)} with the same callback object before registering it
409      * again.
410      *
411      * @param executor an {@link Executor} to execute given callback
412      * @param callback user implementation of the {@link Callback}
413      * @throws NullPointerException if a null executor, or callback is given, or
414      *  IllegalArgumentException if the same <var>callback<var> is already registered.
415      * @hide
416      */
417     @SystemApi
418     @RequiresBluetoothConnectPermission
419     @RequiresPermission(allOf = {
420             android.Manifest.permission.BLUETOOTH_CONNECT,
421             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
422     })
registerCallback(@onNull @allbackExecutor Executor executor, @NonNull Callback callback)423     public void registerCallback(@NonNull @CallbackExecutor Executor executor,
424             @NonNull Callback callback) {
425         Objects.requireNonNull(executor, "executor cannot be null");
426         Objects.requireNonNull(callback, "callback cannot be null");
427 
428         if (DBG) log("registerCallback");
429 
430         synchronized (mCallbackExecutorMap) {
431             // If the callback map is empty, we register the service-to-app callback
432             if (mCallbackExecutorMap.isEmpty()) {
433                 if (!mAdapter.isEnabled()) {
434                     /* If Bluetooth is off, just store callback and it will be registered
435                      * when Bluetooth is on
436                      */
437                     mCallbackExecutorMap.put(callback, executor);
438                     return;
439                 }
440                 try {
441                     final IBluetoothLeAudio service = getService();
442                     if (service != null) {
443                         final SynchronousResultReceiver<Integer> recv =
444                                 SynchronousResultReceiver.get();
445                         service.registerLeBroadcastCallback(mCallback, mAttributionSource, recv);
446                         recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
447                     }
448                 } catch (TimeoutException e) {
449                     Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
450                 } catch (RemoteException e) {
451                     throw e.rethrowFromSystemServer();
452                 }
453             }
454 
455             // Adds the passed in callback to our map of callbacks to executors
456             if (mCallbackExecutorMap.containsKey(callback)) {
457                 throw new IllegalArgumentException("This callback has already been registered");
458             }
459             mCallbackExecutorMap.put(callback, executor);
460         }
461     }
462 
463     /**
464      * Unregister the specified {@link Callback}
465      * <p>The same {@link Callback} object used when calling
466      * {@link #registerCallback(Executor, Callback)} must be used.
467      *
468      * <p>Callbacks are automatically unregistered when application process goes away
469      *
470      * @param callback user implementation of the {@link Callback}
471      * @throws NullPointerException when callback is null or IllegalArgumentException when no
472      *  callback is registered
473      * @hide
474      */
475     @SystemApi
476     @RequiresBluetoothConnectPermission
477     @RequiresPermission(allOf = {
478             android.Manifest.permission.BLUETOOTH_CONNECT,
479             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
480     })
unregisterCallback(@onNull Callback callback)481     public void unregisterCallback(@NonNull Callback callback) {
482         Objects.requireNonNull(callback, "callback cannot be null");
483 
484         if (DBG) log("unregisterCallback");
485 
486         synchronized (mCallbackExecutorMap) {
487             if (mCallbackExecutorMap.remove(callback) == null) {
488                 throw new IllegalArgumentException("This callback has not been registered");
489             }
490         }
491 
492         // If the callback map is empty, we unregister the service-to-app callback
493         if (mCallbackExecutorMap.isEmpty()) {
494             try {
495                 final IBluetoothLeAudio service = getService();
496                 if (service != null) {
497                     final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
498                     service.unregisterLeBroadcastCallback(mCallback, mAttributionSource, recv);
499                     recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
500                 }
501             } catch (TimeoutException | IllegalStateException e) {
502                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
503             } catch (RemoteException e) {
504                 throw e.rethrowFromSystemServer();
505             }
506         }
507     }
508 
509     /**
510      * Start broadcasting to nearby devices using <var>broadcastCode</var> and
511      * <var>contentMetadata</var>
512      *
513      * Encryption will be enabled when <var>broadcastCode</var> is not null.
514      *
515      * <p>As defined in Volume 3, Part C, Section 3.2.6 of Bluetooth Core Specification, Version
516      * 5.3, Broadcast Code is used to encrypt a broadcast audio stream.
517      * <p>It must be a UTF-8 string that has at least 4 octets and should not exceed 16 octets.
518      *
519      * If the provided <var>broadcastCode</var> is non-null and does not meet the above
520      * requirements, encryption will fail to enable with reason code
521      * {@link BluetoothStatusCodes#ERROR_LE_BROADCAST_INVALID_CODE}
522      *
523      * Caller can set content metadata such as program information string in
524      * <var>contentMetadata</var>
525      *
526      * On success, {@link Callback#onBroadcastStarted(int, int)} will be invoked with
527      * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} reason code.
528      * On failure, {@link Callback#onBroadcastStartFailed(int)} will be invoked  with reason code.
529      *
530      * In particular, when the number of Broadcast Sources reaches
531      * {@link #getMaximumNumberOfBroadcast()}, this method will fail with
532      * {@link BluetoothStatusCodes#ERROR_LOCAL_NOT_ENOUGH_RESOURCES}
533      *
534      * After broadcast is started,
535      * {@link Callback#onBroadcastMetadataChanged(int, BluetoothLeBroadcastMetadata)}
536      * will be invoked to expose the latest Broadcast Group metadata that can be shared out of band
537      * to set up Broadcast Sink without scanning.
538      *
539      * Alternatively, one can also get the latest Broadcast Source meta via
540      * {@link #getAllBroadcastMetadata()}
541      *
542      * @param contentMetadata metadata for the default Broadcast subgroup
543      * @param broadcastCode Encryption will be enabled when <var>broadcastCode</var> is not null
544      * @throws IllegalStateException if callback was not registered
545      * @throws NullPointerException if <var>contentMetadata</var> is null
546      * @hide
547      */
548     @SystemApi
549     @RequiresBluetoothConnectPermission
550     @RequiresPermission(allOf = {
551             android.Manifest.permission.BLUETOOTH_CONNECT,
552             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
553     })
startBroadcast(@onNull BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode)554     public void startBroadcast(@NonNull BluetoothLeAudioContentMetadata contentMetadata,
555             @Nullable byte[] broadcastCode) {
556         Objects.requireNonNull(contentMetadata, "contentMetadata cannot be null");
557         if (mCallbackExecutorMap.isEmpty()) {
558             throw new IllegalStateException("No callback was ever registered");
559         }
560 
561         if (DBG) log("startBroadcasting");
562         final IBluetoothLeAudio service = getService();
563         if (service == null) {
564             Log.w(TAG, "Proxy not attached to service");
565             if (DBG) log(Log.getStackTraceString(new Throwable()));
566         } else if (isEnabled()) {
567             try {
568                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
569                 service.startBroadcast(
570                         buildBroadcastSettingsFromMetadata(contentMetadata, broadcastCode),
571                         mAttributionSource, recv);
572                 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
573             } catch (TimeoutException e) {
574                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
575             } catch (RemoteException e) {
576                 throw e.rethrowFromSystemServer();
577             } catch (SecurityException e) {
578                 throw e;
579             }
580         }
581     }
582 
583     /**
584      * Start broadcasting to nearby devices using {@link BluetoothLeBroadcastSettings}.
585      *
586      * @param broadcastSettings broadcast settings for this broadcast group
587      * @throws IllegalStateException if callback was not registered
588      * @throws NullPointerException if <var>broadcastSettings</var> is null
589      * @hide
590      */
591     @SystemApi
592     @RequiresPermission(
593             allOf = {
594                 android.Manifest.permission.BLUETOOTH_CONNECT,
595                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
596             })
startBroadcast(@onNull BluetoothLeBroadcastSettings broadcastSettings)597     public void startBroadcast(@NonNull BluetoothLeBroadcastSettings broadcastSettings) {
598         Objects.requireNonNull(broadcastSettings, "broadcastSettings cannot be null");
599         if (mCallbackExecutorMap.isEmpty()) {
600             throw new IllegalStateException("No callback was ever registered");
601         }
602 
603         if (DBG) log("startBroadcasting");
604         final IBluetoothLeAudio service = getService();
605         if (service == null) {
606             Log.w(TAG, "Proxy not attached to service");
607             if (DBG) log(Log.getStackTraceString(new Throwable()));
608         } else if (isEnabled()) {
609             try {
610                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
611                 service.startBroadcast(broadcastSettings, mAttributionSource, recv);
612                 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
613             } catch (TimeoutException e) {
614                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
615             } catch (RemoteException e) {
616                 throw e.rethrowFromSystemServer();
617             } catch (SecurityException e) {
618                 throw e;
619             }
620         }
621     }
622 
623     /**
624      * Update the broadcast with <var>broadcastId</var> with new <var>contentMetadata</var>
625      *
626      * On success, {@link Callback#onBroadcastUpdated(int, int)} will be invoked with reason code
627      * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}.
628      * On failure, {@link Callback#onBroadcastUpdateFailed(int, int)} will be invoked with reason
629      * code
630      *
631      * @param broadcastId broadcastId as defined by the Basic Audio Profile
632      * @param contentMetadata updated metadata for the default Broadcast subgroup
633      * @throws IllegalStateException if callback was not registered
634      * @throws NullPointerException if <var>contentMetadata</var> is null
635      * @hide
636      */
637     @SystemApi
638     @RequiresBluetoothConnectPermission
639     @RequiresPermission(allOf = {
640             android.Manifest.permission.BLUETOOTH_CONNECT,
641             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
642     })
updateBroadcast(int broadcastId, @NonNull BluetoothLeAudioContentMetadata contentMetadata)643     public void updateBroadcast(int broadcastId,
644             @NonNull BluetoothLeAudioContentMetadata contentMetadata) {
645         Objects.requireNonNull(contentMetadata, "contentMetadata cannot be null");
646         if (mCallbackExecutorMap.isEmpty()) {
647             throw new IllegalStateException("No callback was ever registered");
648         }
649 
650         if (DBG) log("updateBroadcast");
651         final IBluetoothLeAudio service = getService();
652         if (service == null) {
653             Log.w(TAG, "Proxy not attached to service");
654             if (DBG) log(Log.getStackTraceString(new Throwable()));
655         } else if (isEnabled()) {
656             try {
657                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
658                 service.updateBroadcast(broadcastId,
659                         buildBroadcastSettingsFromMetadata(contentMetadata, null),
660                         mAttributionSource, recv);
661                 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
662             } catch (TimeoutException e) {
663                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
664             } catch (RemoteException e) {
665                 throw e.rethrowFromSystemServer();
666             } catch (SecurityException e) {
667                 throw e;
668             }
669         }
670     }
671 
672     /**
673      * Update the broadcast with <var>broadcastId</var> with <var>BluetoothLeBroadcastSettings</var>
674      *
675      * <p>On success, {@link Callback#onBroadcastUpdated(int, int)} will be invoked with reason code
676      * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST}. On failure, {@link
677      * Callback#onBroadcastUpdateFailed(int, int)} will be invoked with reason code
678      *
679      * @param broadcastId broadcastId as defined by the Basic Audio Profile
680      * @param broadcastSettings broadcast settings for this broadcast group
681      * @throws IllegalStateException if callback was not registered
682      * @throws NullPointerException if <var>broadcastSettings</var> is null
683      * @hide
684      */
685     @SystemApi
686     @RequiresPermission(
687             allOf = {
688                 android.Manifest.permission.BLUETOOTH_CONNECT,
689                 android.Manifest.permission.BLUETOOTH_PRIVILEGED,
690             })
updateBroadcast( int broadcastId, @NonNull BluetoothLeBroadcastSettings broadcastSettings)691     public void updateBroadcast(
692             int broadcastId, @NonNull BluetoothLeBroadcastSettings broadcastSettings) {
693         Objects.requireNonNull(broadcastSettings, "broadcastSettings cannot be null");
694         if (mCallbackExecutorMap.isEmpty()) {
695             throw new IllegalStateException("No callback was ever registered");
696         }
697 
698         if (DBG) log("updateBroadcast");
699         final IBluetoothLeAudio service = getService();
700         if (service == null) {
701             Log.w(TAG, "Proxy not attached to service");
702             if (DBG) log(Log.getStackTraceString(new Throwable()));
703         } else if (isEnabled()) {
704             try {
705                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
706                 service.updateBroadcast(broadcastId, broadcastSettings, mAttributionSource, recv);
707                 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
708             } catch (TimeoutException e) {
709                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
710             } catch (RemoteException e) {
711                 throw e.rethrowFromSystemServer();
712             } catch (SecurityException e) {
713                 throw e;
714             }
715         }
716     }
717 
718     /**
719      * Stop broadcasting.
720      *
721      * On success, {@link Callback#onBroadcastStopped(int, int)} will be invoked with reason code
722      * {@link BluetoothStatusCodes#REASON_LOCAL_APP_REQUEST} and the <var>broadcastId</var>
723      * On failure, {@link Callback#onBroadcastStopFailed(int)} will be invoked with reason code
724      *
725      * @param broadcastId as defined by the Basic Audio Profile
726      * @throws IllegalStateException if callback was not registered
727      * @hide
728      */
729     @SystemApi
730     @RequiresBluetoothConnectPermission
731     @RequiresPermission(allOf = {
732             android.Manifest.permission.BLUETOOTH_CONNECT,
733             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
734     })
stopBroadcast(int broadcastId)735     public void stopBroadcast(int broadcastId) {
736         if (mCallbackExecutorMap.isEmpty()) {
737             throw new IllegalStateException("No callback was ever registered");
738         }
739 
740         if (DBG) log("disableBroadcastMode");
741         final IBluetoothLeAudio service = getService();
742         if (service == null) {
743             Log.w(TAG, "Proxy not attached to service");
744             if (DBG) log(Log.getStackTraceString(new Throwable()));
745         } else if (isEnabled()) {
746             try {
747                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
748                 service.stopBroadcast(broadcastId, mAttributionSource, recv);
749                 recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(null);
750             } catch (TimeoutException e) {
751                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
752             } catch (RemoteException e) {
753                 throw e.rethrowFromSystemServer();
754             } catch (SecurityException e) {
755                 throw e;
756             }
757         }
758     }
759 
760     /**
761      * Return true if audio is being broadcasted on the Broadcast Source as identified by the
762      * <var>broadcastId</var>
763      *
764      * @param broadcastId as defined in the Basic Audio Profile
765      * @return true if audio is being broadcasted
766      * @hide
767      */
768     @SystemApi
769     @RequiresBluetoothConnectPermission
770     @RequiresPermission(allOf = {
771             android.Manifest.permission.BLUETOOTH_CONNECT,
772             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
773     })
isPlaying(int broadcastId)774     public boolean isPlaying(int broadcastId) {
775         final IBluetoothLeAudio service = getService();
776         final boolean defaultValue = false;
777         if (service == null) {
778             Log.w(TAG, "Proxy not attached to service");
779             if (DBG) log(Log.getStackTraceString(new Throwable()));
780         } else if (isEnabled()) {
781             try {
782                 final SynchronousResultReceiver<Boolean> recv = SynchronousResultReceiver.get();
783                 service.isPlaying(broadcastId, mAttributionSource, recv);
784                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
785             } catch (TimeoutException e) {
786                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
787             } catch (RemoteException e) {
788                 throw e.rethrowFromSystemServer();
789             }
790         }
791         return defaultValue;
792     }
793 
794     /**
795      * Get {@link BluetoothLeBroadcastMetadata} for all Broadcast Groups currently running on
796      * this device
797      *
798      * @return list of {@link BluetoothLeBroadcastMetadata}
799      * @hide
800      */
801     @SystemApi
802     @RequiresBluetoothConnectPermission
803     @RequiresPermission(allOf = {
804             android.Manifest.permission.BLUETOOTH_CONNECT,
805             android.Manifest.permission.BLUETOOTH_PRIVILEGED,
806     })
getAllBroadcastMetadata()807     public @NonNull List<BluetoothLeBroadcastMetadata> getAllBroadcastMetadata() {
808         final IBluetoothLeAudio service = getService();
809         final List<BluetoothLeBroadcastMetadata> defaultValue = Collections.emptyList();
810         if (service == null) {
811             Log.w(TAG, "Proxy not attached to service");
812             if (DBG) log(Log.getStackTraceString(new Throwable()));
813         } else if (isEnabled()) {
814             try {
815                 final SynchronousResultReceiver<List<BluetoothLeBroadcastMetadata>> recv =
816                         SynchronousResultReceiver.get();
817                 service.getAllBroadcastMetadata(mAttributionSource, recv);
818                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
819             } catch (TimeoutException e) {
820                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
821             } catch (RemoteException e) {
822                 throw e.rethrowFromSystemServer();
823             }
824         }
825         return defaultValue;
826     }
827 
828     /**
829      * Get the maximum number of Broadcast Isochronous Group supported on this device
830      *
831      * @return maximum number of Broadcast Isochronous Group supported on this device
832      * @hide
833      */
834     @SystemApi
835     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getMaximumNumberOfBroadcasts()836     public int getMaximumNumberOfBroadcasts() {
837         final IBluetoothLeAudio service = getService();
838         final int defaultValue = 1;
839         if (service == null) {
840             Log.w(TAG, "Proxy not attached to service");
841             if (DBG) log(Log.getStackTraceString(new Throwable()));
842         } else if (isEnabled()) {
843             try {
844                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
845                 service.getMaximumNumberOfBroadcasts(mAttributionSource, recv);
846                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
847             } catch (TimeoutException e) {
848                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
849             } catch (RemoteException e) {
850                 throw e.rethrowFromSystemServer();
851             }
852         }
853         return defaultValue;
854     }
855 
856     /**
857      * Get the maximum number of streams per broadcast
858      * Single stream means single Audio PCM stream
859      *
860      * @return maximum number of broadcast streams per broadcast group
861      * @hide
862      */
863     @SystemApi
864     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getMaximumStreamsPerBroadcast()865     public int getMaximumStreamsPerBroadcast() {
866         final IBluetoothLeAudio service = getService();
867         final int defaultValue = 1;
868         if (service == null) {
869             Log.w(TAG, "Proxy not attached to service");
870             if (DBG) log(Log.getStackTraceString(new Throwable()));
871         } else if (isEnabled()) {
872             try {
873                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
874                 service.getMaximumStreamsPerBroadcast(mAttributionSource, recv);
875                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
876             } catch (TimeoutException e) {
877                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
878             } catch (RemoteException e) {
879                 throw e.rethrowFromSystemServer();
880             }
881         }
882         return defaultValue;
883     }
884 
885     /**
886      * Get the maximum number of subgroups per broadcast
887      * Single stream means single Audio PCM stream, one stream could support
888      * single or multiple subgroups based on language and audio configuration.
889      * e.g. Stream 1 -> 2 subgroups with English and Spanish, Stream 2 -> 1 subgroups
890      * with English, Stream 3 -> 2 subgroups with hearing Aids Standard and High Quality
891      *
892      * @return maximum number of broadcast subgroups per broadcast group
893      * @hide
894      */
895     @SystemApi
896     @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
getMaximumSubgroupsPerBroadcast()897     public int getMaximumSubgroupsPerBroadcast() {
898         final IBluetoothLeAudio service = getService();
899         final int defaultValue = 1;
900         if (service == null) {
901             Log.w(TAG, "Proxy not attached to service");
902             if (DBG) log(Log.getStackTraceString(new Throwable()));
903         } else if (isEnabled()) {
904             try {
905                 final SynchronousResultReceiver<Integer> recv = SynchronousResultReceiver.get();
906                 service.getMaximumSubgroupsPerBroadcast(mAttributionSource, recv);
907                 return recv.awaitResultNoInterrupt(getSyncTimeout()).getValue(defaultValue);
908             } catch (TimeoutException e) {
909                 Log.e(TAG, e.toString() + "\n" + Log.getStackTraceString(new Throwable()));
910             } catch (RemoteException e) {
911                 throw e.rethrowFromSystemServer();
912             }
913         }
914         return defaultValue;
915     }
916 
917     /**
918      * {@inheritDoc}
919      * @hide
920      */
921     @Override
close()922     public void close() {
923         if (VDBG) log("close()");
924 
925         mProfileConnector.disconnect();
926     }
927 
buildBroadcastSettingsFromMetadata( BluetoothLeAudioContentMetadata contentMetadata, @Nullable byte[] broadcastCode)928     private BluetoothLeBroadcastSettings buildBroadcastSettingsFromMetadata(
929             BluetoothLeAudioContentMetadata contentMetadata,
930             @Nullable byte[] broadcastCode) {
931         BluetoothLeBroadcastSubgroupSettings.Builder subgroupBuilder =
932                 new BluetoothLeBroadcastSubgroupSettings.Builder()
933                 .setContentMetadata(contentMetadata);
934 
935         BluetoothLeBroadcastSettings.Builder builder = new BluetoothLeBroadcastSettings.Builder()
936                         .setPublicBroadcast(false)
937                         .setBroadcastCode(broadcastCode);
938         // builder expect at least one subgroup setting
939         builder.addSubgroupSettings(subgroupBuilder.build());
940         return builder.build();
941     }
942 
isEnabled()943     private boolean isEnabled() {
944         if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true;
945         return false;
946     }
947 
getService()948     private IBluetoothLeAudio getService() {
949         return mProfileConnector.getService();
950     }
951 
log(String msg)952     private static void log(String msg) {
953         Log.d(TAG, msg);
954     }
955 }
956