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