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