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