1 /* 2 * Copyright (C) 2012 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 com.android.bluetooth.a2dp; 18 19 import android.bluetooth.BluetoothA2dp; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothCodecConfig; 22 import android.bluetooth.BluetoothCodecStatus; 23 import android.bluetooth.BluetoothDevice; 24 import android.bluetooth.BluetoothProfile; 25 import android.bluetooth.BluetoothUuid; 26 import android.bluetooth.IBluetoothA2dp; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.media.AudioManager; 32 import android.os.HandlerThread; 33 import android.provider.Settings; 34 import android.support.annotation.GuardedBy; 35 import android.support.annotation.VisibleForTesting; 36 import android.util.Log; 37 38 import com.android.bluetooth.BluetoothMetricsProto; 39 import com.android.bluetooth.Utils; 40 import com.android.bluetooth.avrcp.Avrcp; 41 import com.android.bluetooth.avrcp.AvrcpTargetService; 42 import com.android.bluetooth.btservice.AdapterService; 43 import com.android.bluetooth.btservice.MetricsLogger; 44 import com.android.bluetooth.btservice.ProfileService; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Objects; 49 import java.util.Set; 50 import java.util.concurrent.ConcurrentHashMap; 51 import java.util.concurrent.ConcurrentMap; 52 53 /** 54 * Provides Bluetooth A2DP profile, as a service in the Bluetooth application. 55 * @hide 56 */ 57 public class A2dpService extends ProfileService { 58 private static final boolean DBG = true; 59 private static final String TAG = "A2dpService"; 60 61 private static A2dpService sA2dpService; 62 63 private BluetoothAdapter mAdapter; 64 private AdapterService mAdapterService; 65 private HandlerThread mStateMachinesThread; 66 private Avrcp mAvrcp; 67 68 @VisibleForTesting 69 A2dpNativeInterface mA2dpNativeInterface; 70 private AudioManager mAudioManager; 71 private A2dpCodecConfig mA2dpCodecConfig; 72 73 @GuardedBy("mStateMachines") 74 private BluetoothDevice mActiveDevice; 75 private final ConcurrentMap<BluetoothDevice, A2dpStateMachine> mStateMachines = 76 new ConcurrentHashMap<>(); 77 78 // Upper limit of all A2DP devices: Bonded or Connected 79 private static final int MAX_A2DP_STATE_MACHINES = 50; 80 // Upper limit of all A2DP devices that are Connected or Connecting 81 private int mMaxConnectedAudioDevices = 1; 82 // A2DP Offload Enabled in platform 83 boolean mA2dpOffloadEnabled = false; 84 85 private BroadcastReceiver mBondStateChangedReceiver; 86 private BroadcastReceiver mConnectionStateChangedReceiver; 87 88 @Override initBinder()89 protected IProfileServiceBinder initBinder() { 90 return new BluetoothA2dpBinder(this); 91 } 92 93 @Override create()94 protected void create() { 95 Log.i(TAG, "create()"); 96 } 97 98 @Override start()99 protected boolean start() { 100 Log.i(TAG, "start()"); 101 if (sA2dpService != null) { 102 throw new IllegalStateException("start() called twice"); 103 } 104 105 // Step 1: Get BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager. 106 // None of them can be null. 107 mAdapter = Objects.requireNonNull(BluetoothAdapter.getDefaultAdapter(), 108 "BluetoothAdapter cannot be null when A2dpService starts"); 109 mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), 110 "AdapterService cannot be null when A2dpService starts"); 111 mA2dpNativeInterface = Objects.requireNonNull(A2dpNativeInterface.getInstance(), 112 "A2dpNativeInterface cannot be null when A2dpService starts"); 113 mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); 114 Objects.requireNonNull(mAudioManager, 115 "AudioManager cannot be null when A2dpService starts"); 116 117 // Step 2: Get maximum number of connected audio devices 118 mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices(); 119 Log.i(TAG, "Max connected audio devices set to " + mMaxConnectedAudioDevices); 120 121 // Step 3: Setup AVRCP 122 mAvrcp = Avrcp.make(this); 123 124 // Step 4: Start handler thread for state machines 125 mStateMachines.clear(); 126 mStateMachinesThread = new HandlerThread("A2dpService.StateMachines"); 127 mStateMachinesThread.start(); 128 129 // Step 5: Setup codec config 130 mA2dpCodecConfig = new A2dpCodecConfig(this, mA2dpNativeInterface); 131 132 // Step 6: Initialize native interface 133 mA2dpNativeInterface.init(mMaxConnectedAudioDevices, 134 mA2dpCodecConfig.codecConfigPriorities()); 135 136 // Step 7: Check if A2DP is in offload mode 137 mA2dpOffloadEnabled = mAdapterService.isA2dpOffloadEnabled(); 138 if (DBG) { 139 Log.d(TAG, "A2DP offload flag set to " + mA2dpOffloadEnabled); 140 } 141 142 // Step 8: Setup broadcast receivers 143 IntentFilter filter = new IntentFilter(); 144 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 145 mBondStateChangedReceiver = new BondStateChangedReceiver(); 146 registerReceiver(mBondStateChangedReceiver, filter); 147 filter = new IntentFilter(); 148 filter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 149 mConnectionStateChangedReceiver = new ConnectionStateChangedReceiver(); 150 registerReceiver(mConnectionStateChangedReceiver, filter); 151 152 // Step 9: Mark service as started 153 setA2dpService(this); 154 155 // Step 10: Clear active device 156 setActiveDevice(null); 157 158 return true; 159 } 160 161 @Override stop()162 protected boolean stop() { 163 Log.i(TAG, "stop()"); 164 if (sA2dpService == null) { 165 Log.w(TAG, "stop() called before start()"); 166 return true; 167 } 168 169 // Step 10: Store volume if there is an active device 170 if (mActiveDevice != null && AvrcpTargetService.get() != null) { 171 AvrcpTargetService.get().storeVolumeForDevice(mActiveDevice); 172 } 173 174 // Step 9: Clear active device and stop playing audio 175 removeActiveDevice(true); 176 177 // Step 8: Mark service as stopped 178 setA2dpService(null); 179 180 // Step 7: Unregister broadcast receivers 181 unregisterReceiver(mConnectionStateChangedReceiver); 182 mConnectionStateChangedReceiver = null; 183 unregisterReceiver(mBondStateChangedReceiver); 184 mBondStateChangedReceiver = null; 185 186 // Step 6: Cleanup native interface 187 mA2dpNativeInterface.cleanup(); 188 mA2dpNativeInterface = null; 189 190 // Step 5: Clear codec config 191 mA2dpCodecConfig = null; 192 193 // Step 4: Destroy state machines and stop handler thread 194 synchronized (mStateMachines) { 195 for (A2dpStateMachine sm : mStateMachines.values()) { 196 sm.doQuit(); 197 sm.cleanup(); 198 } 199 mStateMachines.clear(); 200 } 201 mStateMachinesThread.quitSafely(); 202 mStateMachinesThread = null; 203 204 // Step 3: Cleanup AVRCP 205 mAvrcp.doQuit(); 206 mAvrcp.cleanup(); 207 mAvrcp = null; 208 209 // Step 2: Reset maximum number of connected audio devices 210 mMaxConnectedAudioDevices = 1; 211 212 // Step 1: Clear BluetoothAdapter, AdapterService, A2dpNativeInterface, AudioManager 213 mAudioManager = null; 214 mA2dpNativeInterface = null; 215 mAdapterService = null; 216 mAdapter = null; 217 218 return true; 219 } 220 221 @Override cleanup()222 protected void cleanup() { 223 Log.i(TAG, "cleanup()"); 224 } 225 getA2dpService()226 public static synchronized A2dpService getA2dpService() { 227 if (sA2dpService == null) { 228 Log.w(TAG, "getA2dpService(): service is null"); 229 return null; 230 } 231 if (!sA2dpService.isAvailable()) { 232 Log.w(TAG, "getA2dpService(): service is not available"); 233 return null; 234 } 235 return sA2dpService; 236 } 237 setA2dpService(A2dpService instance)238 private static synchronized void setA2dpService(A2dpService instance) { 239 if (DBG) { 240 Log.d(TAG, "setA2dpService(): set to: " + instance); 241 } 242 sA2dpService = instance; 243 } 244 connect(BluetoothDevice device)245 public boolean connect(BluetoothDevice device) { 246 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 247 if (DBG) { 248 Log.d(TAG, "connect(): " + device); 249 } 250 251 if (getPriority(device) == BluetoothProfile.PRIORITY_OFF) { 252 Log.e(TAG, "Cannot connect to " + device + " : PRIORITY_OFF"); 253 return false; 254 } 255 if (!BluetoothUuid.isUuidPresent(mAdapterService.getRemoteUuids(device), 256 BluetoothUuid.AudioSink)) { 257 Log.e(TAG, "Cannot connect to " + device + " : Remote does not have A2DP Sink UUID"); 258 return false; 259 } 260 261 synchronized (mStateMachines) { 262 if (!connectionAllowedCheckMaxDevices(device)) { 263 Log.e(TAG, "Cannot connect to " + device + " : too many connected devices"); 264 return false; 265 } 266 A2dpStateMachine smConnect = getOrCreateStateMachine(device); 267 if (smConnect == null) { 268 Log.e(TAG, "Cannot connect to " + device + " : no state machine"); 269 return false; 270 } 271 smConnect.sendMessage(A2dpStateMachine.CONNECT); 272 return true; 273 } 274 } 275 disconnect(BluetoothDevice device)276 boolean disconnect(BluetoothDevice device) { 277 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 278 if (DBG) { 279 Log.d(TAG, "disconnect(): " + device); 280 } 281 282 synchronized (mStateMachines) { 283 A2dpStateMachine sm = mStateMachines.get(device); 284 if (sm == null) { 285 Log.e(TAG, "Ignored disconnect request for " + device + " : no state machine"); 286 return false; 287 } 288 sm.sendMessage(A2dpStateMachine.DISCONNECT); 289 return true; 290 } 291 } 292 getConnectedDevices()293 public List<BluetoothDevice> getConnectedDevices() { 294 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 295 synchronized (mStateMachines) { 296 List<BluetoothDevice> devices = new ArrayList<>(); 297 for (A2dpStateMachine sm : mStateMachines.values()) { 298 if (sm.isConnected()) { 299 devices.add(sm.getDevice()); 300 } 301 } 302 return devices; 303 } 304 } 305 306 /** 307 * Check whether can connect to a peer device. 308 * The check considers the maximum number of connected peers. 309 * 310 * @param device the peer device to connect to 311 * @return true if connection is allowed, otherwise false 312 */ connectionAllowedCheckMaxDevices(BluetoothDevice device)313 private boolean connectionAllowedCheckMaxDevices(BluetoothDevice device) { 314 int connected = 0; 315 // Count devices that are in the process of connecting or already connected 316 synchronized (mStateMachines) { 317 for (A2dpStateMachine sm : mStateMachines.values()) { 318 switch (sm.getConnectionState()) { 319 case BluetoothProfile.STATE_CONNECTING: 320 case BluetoothProfile.STATE_CONNECTED: 321 if (Objects.equals(device, sm.getDevice())) { 322 return true; // Already connected or accounted for 323 } 324 connected++; 325 break; 326 default: 327 break; 328 } 329 } 330 } 331 return (connected < mMaxConnectedAudioDevices); 332 } 333 334 /** 335 * Check whether can connect to a peer device. 336 * The check considers a number of factors during the evaluation. 337 * 338 * @param device the peer device to connect to 339 * @param isOutgoingRequest if true, the check is for outgoing connection 340 * request, otherwise is for incoming connection request 341 * @return true if connection is allowed, otherwise false 342 */ 343 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) okToConnect(BluetoothDevice device, boolean isOutgoingRequest)344 public boolean okToConnect(BluetoothDevice device, boolean isOutgoingRequest) { 345 Log.i(TAG, "okToConnect: device " + device + " isOutgoingRequest: " + isOutgoingRequest); 346 // Check if this is an incoming connection in Quiet mode. 347 if (mAdapterService.isQuietModeEnabled() && !isOutgoingRequest) { 348 Log.e(TAG, "okToConnect: cannot connect to " + device + " : quiet mode enabled"); 349 return false; 350 } 351 // Check if too many devices 352 if (!connectionAllowedCheckMaxDevices(device)) { 353 Log.e(TAG, "okToConnect: cannot connect to " + device 354 + " : too many connected devices"); 355 return false; 356 } 357 // Check priority and accept or reject the connection. 358 int priority = getPriority(device); 359 int bondState = mAdapterService.getBondState(device); 360 // Allow this connection only if the device is bonded. Any attempt to connect while 361 // bonding would potentially lead to an unauthorized connection. 362 if (bondState != BluetoothDevice.BOND_BONDED) { 363 Log.w(TAG, "okToConnect: return false, bondState=" + bondState); 364 return false; 365 } else if (priority != BluetoothProfile.PRIORITY_UNDEFINED 366 && priority != BluetoothProfile.PRIORITY_ON 367 && priority != BluetoothProfile.PRIORITY_AUTO_CONNECT) { 368 // Otherwise, reject the connection if priority is not valid. 369 Log.w(TAG, "okToConnect: return false, priority=" + priority); 370 return false; 371 } 372 return true; 373 } 374 getDevicesMatchingConnectionStates(int[] states)375 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 376 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 377 List<BluetoothDevice> devices = new ArrayList<>(); 378 Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); 379 synchronized (mStateMachines) { 380 for (BluetoothDevice device : bondedDevices) { 381 if (!BluetoothUuid.isUuidPresent(mAdapterService.getRemoteUuids(device), 382 BluetoothUuid.AudioSink)) { 383 continue; 384 } 385 int connectionState = BluetoothProfile.STATE_DISCONNECTED; 386 A2dpStateMachine sm = mStateMachines.get(device); 387 if (sm != null) { 388 connectionState = sm.getConnectionState(); 389 } 390 for (int i = 0; i < states.length; i++) { 391 if (connectionState == states[i]) { 392 devices.add(device); 393 } 394 } 395 } 396 return devices; 397 } 398 } 399 400 /** 401 * Get the list of devices that have state machines. 402 * 403 * @return the list of devices that have state machines 404 */ 405 @VisibleForTesting getDevices()406 List<BluetoothDevice> getDevices() { 407 List<BluetoothDevice> devices = new ArrayList<>(); 408 synchronized (mStateMachines) { 409 for (A2dpStateMachine sm : mStateMachines.values()) { 410 devices.add(sm.getDevice()); 411 } 412 return devices; 413 } 414 } 415 getConnectionState(BluetoothDevice device)416 public int getConnectionState(BluetoothDevice device) { 417 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 418 synchronized (mStateMachines) { 419 A2dpStateMachine sm = mStateMachines.get(device); 420 if (sm == null) { 421 return BluetoothProfile.STATE_DISCONNECTED; 422 } 423 return sm.getConnectionState(); 424 } 425 } 426 removeActiveDevice(boolean forceStopPlayingAudio)427 private void removeActiveDevice(boolean forceStopPlayingAudio) { 428 BluetoothDevice previousActiveDevice = mActiveDevice; 429 synchronized (mStateMachines) { 430 // Clear the active device 431 mActiveDevice = null; 432 // This needs to happen before we inform the audio manager that the device 433 // disconnected. Please see comment in broadcastActiveDevice() for why. 434 broadcastActiveDevice(null); 435 436 if (previousActiveDevice == null) { 437 return; 438 } 439 440 // Make sure the Audio Manager knows the previous Active device is disconnected. 441 // However, if A2DP is still connected and not forcing stop audio for that remote 442 // device, the user has explicitly switched the output to the local device and music 443 // should continue playing. Otherwise, the remote device has been indeed disconnected 444 // and audio should be suspended before switching the output to the local device. 445 boolean suppressNoisyIntent = !forceStopPlayingAudio 446 && (getConnectionState(previousActiveDevice) 447 == BluetoothProfile.STATE_CONNECTED); 448 Log.i(TAG, "removeActiveDevice: suppressNoisyIntent=" + suppressNoisyIntent); 449 mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( 450 previousActiveDevice, BluetoothProfile.STATE_DISCONNECTED, 451 BluetoothProfile.A2DP, suppressNoisyIntent, -1); 452 // Make sure the Active device in native layer is set to null and audio is off 453 if (!mA2dpNativeInterface.setActiveDevice(null)) { 454 Log.w(TAG, "setActiveDevice(null): Cannot remove active device in native " 455 + "layer"); 456 } 457 } 458 } 459 460 /** 461 * Set the active device. 462 * 463 * @param device the active device 464 * @return true on success, otherwise false 465 */ setActiveDevice(BluetoothDevice device)466 public boolean setActiveDevice(BluetoothDevice device) { 467 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH ADMIN permission"); 468 synchronized (mStateMachines) { 469 BluetoothDevice previousActiveDevice = mActiveDevice; 470 if (DBG) { 471 Log.d(TAG, "setActiveDevice(" + device + "): previous is " + previousActiveDevice); 472 } 473 474 if (previousActiveDevice != null && AvrcpTargetService.get() != null) { 475 AvrcpTargetService.get().storeVolumeForDevice(previousActiveDevice); 476 } 477 478 if (device == null) { 479 // Remove active device and continue playing audio only if necessary. 480 removeActiveDevice(false); 481 return true; 482 } 483 484 BluetoothCodecStatus codecStatus = null; 485 A2dpStateMachine sm = mStateMachines.get(device); 486 if (sm == null) { 487 Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active: " 488 + "no state machine"); 489 return false; 490 } 491 if (sm.getConnectionState() != BluetoothProfile.STATE_CONNECTED) { 492 Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active: " 493 + "device is not connected"); 494 return false; 495 } 496 if (!mA2dpNativeInterface.setActiveDevice(device)) { 497 Log.e(TAG, "setActiveDevice(" + device + "): Cannot set as active in native layer"); 498 return false; 499 } 500 codecStatus = sm.getCodecStatus(); 501 502 boolean deviceChanged = !Objects.equals(device, mActiveDevice); 503 mActiveDevice = device; 504 // This needs to happen before we inform the audio manager that the device 505 // disconnected. Please see comment in broadcastActiveDevice() for why. 506 broadcastActiveDevice(mActiveDevice); 507 if (deviceChanged) { 508 // Send an intent with the active device codec config 509 if (codecStatus != null) { 510 broadcastCodecConfig(mActiveDevice, codecStatus); 511 } 512 // Make sure the Audio Manager knows the previous Active device is disconnected, 513 // and the new Active device is connected. 514 if (previousActiveDevice != null) { 515 mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( 516 previousActiveDevice, BluetoothProfile.STATE_DISCONNECTED, 517 BluetoothProfile.A2DP, true, -1); 518 } 519 520 int rememberedVolume = -1; 521 if (AvrcpTargetService.get() != null) { 522 AvrcpTargetService.get().volumeDeviceSwitched(mActiveDevice); 523 524 rememberedVolume = AvrcpTargetService.get() 525 .getRememberedVolumeForDevice(mActiveDevice); 526 } 527 528 mAudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent( 529 mActiveDevice, BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP, 530 true, rememberedVolume); 531 532 // Inform the Audio Service about the codec configuration 533 // change, so the Audio Service can reset accordingly the audio 534 // feeding parameters in the Audio HAL to the Bluetooth stack. 535 mAudioManager.handleBluetoothA2dpDeviceConfigChange(mActiveDevice); 536 } 537 } 538 return true; 539 } 540 541 /** 542 * Get the active device. 543 * 544 * @return the active device or null if no device is active 545 */ getActiveDevice()546 public BluetoothDevice getActiveDevice() { 547 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 548 synchronized (mStateMachines) { 549 return mActiveDevice; 550 } 551 } 552 isActiveDevice(BluetoothDevice device)553 private boolean isActiveDevice(BluetoothDevice device) { 554 synchronized (mStateMachines) { 555 return (device != null) && Objects.equals(device, mActiveDevice); 556 } 557 } 558 setPriority(BluetoothDevice device, int priority)559 public boolean setPriority(BluetoothDevice device, int priority) { 560 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 561 Settings.Global.putInt(getContentResolver(), 562 Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); 563 if (DBG) { 564 Log.d(TAG, "Saved priority " + device + " = " + priority); 565 } 566 return true; 567 } 568 getPriority(BluetoothDevice device)569 public int getPriority(BluetoothDevice device) { 570 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 571 int priority = Settings.Global.getInt(getContentResolver(), 572 Settings.Global.getBluetoothA2dpSinkPriorityKey(device.getAddress()), 573 BluetoothProfile.PRIORITY_UNDEFINED); 574 return priority; 575 } 576 577 /* Absolute volume implementation */ isAvrcpAbsoluteVolumeSupported()578 public boolean isAvrcpAbsoluteVolumeSupported() { 579 return mAvrcp.isAbsoluteVolumeSupported(); 580 } 581 setAvrcpAbsoluteVolume(int volume)582 public void setAvrcpAbsoluteVolume(int volume) { 583 // TODO (apanicke): Instead of using A2DP as a middleman for volume changes, add a binder 584 // service to the new AVRCP Profile and have the audio manager use that instead. 585 if (AvrcpTargetService.get() != null) { 586 AvrcpTargetService.get().sendVolumeChanged(volume); 587 return; 588 } 589 590 mAvrcp.setAbsoluteVolume(volume); 591 } 592 setAvrcpAudioState(int state)593 public void setAvrcpAudioState(int state) { 594 mAvrcp.setA2dpAudioState(state); 595 } 596 resetAvrcpBlacklist(BluetoothDevice device)597 public void resetAvrcpBlacklist(BluetoothDevice device) { 598 if (mAvrcp != null) { 599 mAvrcp.resetBlackList(device.getAddress()); 600 } 601 } 602 isA2dpPlaying(BluetoothDevice device)603 boolean isA2dpPlaying(BluetoothDevice device) { 604 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 605 if (DBG) { 606 Log.d(TAG, "isA2dpPlaying(" + device + ")"); 607 } 608 synchronized (mStateMachines) { 609 A2dpStateMachine sm = mStateMachines.get(device); 610 if (sm == null) { 611 return false; 612 } 613 return sm.isPlaying(); 614 } 615 } 616 617 /** 618 * Gets the current codec status (configuration and capability). 619 * 620 * @param device the remote Bluetooth device. If null, use the currect 621 * active A2DP Bluetooth device. 622 * @return the current codec status 623 * @hide 624 */ getCodecStatus(BluetoothDevice device)625 public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) { 626 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 627 if (DBG) { 628 Log.d(TAG, "getCodecStatus(" + device + ")"); 629 } 630 synchronized (mStateMachines) { 631 if (device == null) { 632 device = mActiveDevice; 633 } 634 if (device == null) { 635 return null; 636 } 637 A2dpStateMachine sm = mStateMachines.get(device); 638 if (sm != null) { 639 return sm.getCodecStatus(); 640 } 641 return null; 642 } 643 } 644 645 /** 646 * Sets the codec configuration preference. 647 * 648 * @param device the remote Bluetooth device. If null, use the currect 649 * active A2DP Bluetooth device. 650 * @param codecConfig the codec configuration preference 651 * @hide 652 */ setCodecConfigPreference(BluetoothDevice device, BluetoothCodecConfig codecConfig)653 public void setCodecConfigPreference(BluetoothDevice device, 654 BluetoothCodecConfig codecConfig) { 655 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 656 if (DBG) { 657 Log.d(TAG, "setCodecConfigPreference(" + device + "): " 658 + Objects.toString(codecConfig)); 659 } 660 if (device == null) { 661 device = mActiveDevice; 662 } 663 if (device == null) { 664 Log.e(TAG, "Cannot set codec config preference: no active A2DP device"); 665 return; 666 } 667 mA2dpCodecConfig.setCodecConfigPreference(device, codecConfig); 668 } 669 670 /** 671 * Enables the optional codecs. 672 * 673 * @param device the remote Bluetooth device. If null, use the currect 674 * active A2DP Bluetooth device. 675 * @hide 676 */ enableOptionalCodecs(BluetoothDevice device)677 public void enableOptionalCodecs(BluetoothDevice device) { 678 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 679 if (DBG) { 680 Log.d(TAG, "enableOptionalCodecs(" + device + ")"); 681 } 682 if (device == null) { 683 device = mActiveDevice; 684 } 685 if (device == null) { 686 Log.e(TAG, "Cannot enable optional codecs: no active A2DP device"); 687 return; 688 } 689 mA2dpCodecConfig.enableOptionalCodecs(device); 690 } 691 692 /** 693 * Disables the optional codecs. 694 * 695 * @param device the remote Bluetooth device. If null, use the currect 696 * active A2DP Bluetooth device. 697 * @hide 698 */ disableOptionalCodecs(BluetoothDevice device)699 public void disableOptionalCodecs(BluetoothDevice device) { 700 enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 701 if (DBG) { 702 Log.d(TAG, "disableOptionalCodecs(" + device + ")"); 703 } 704 if (device == null) { 705 device = mActiveDevice; 706 } 707 if (device == null) { 708 Log.e(TAG, "Cannot disable optional codecs: no active A2DP device"); 709 return; 710 } 711 mA2dpCodecConfig.disableOptionalCodecs(device); 712 } 713 getSupportsOptionalCodecs(BluetoothDevice device)714 public int getSupportsOptionalCodecs(BluetoothDevice device) { 715 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 716 int support = Settings.Global.getInt(getContentResolver(), 717 Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()), 718 BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN); 719 return support; 720 } 721 setSupportsOptionalCodecs(BluetoothDevice device, boolean doesSupport)722 public void setSupportsOptionalCodecs(BluetoothDevice device, boolean doesSupport) { 723 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 724 int value = doesSupport ? BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED 725 : BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED; 726 Settings.Global.putInt(getContentResolver(), 727 Settings.Global.getBluetoothA2dpSupportsOptionalCodecsKey(device.getAddress()), 728 value); 729 } 730 getOptionalCodecsEnabled(BluetoothDevice device)731 public int getOptionalCodecsEnabled(BluetoothDevice device) { 732 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 733 return Settings.Global.getInt(getContentResolver(), 734 Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()), 735 BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN); 736 } 737 setOptionalCodecsEnabled(BluetoothDevice device, int value)738 public void setOptionalCodecsEnabled(BluetoothDevice device, int value) { 739 enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, "Need BLUETOOTH_ADMIN permission"); 740 if (value != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN 741 && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED 742 && value != BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) { 743 Log.w(TAG, "Unexpected value passed to setOptionalCodecsEnabled:" + value); 744 return; 745 } 746 Settings.Global.putInt(getContentResolver(), 747 Settings.Global.getBluetoothA2dpOptionalCodecsEnabledKey(device.getAddress()), 748 value); 749 } 750 751 // Handle messages from native (JNI) to Java messageFromNative(A2dpStackEvent stackEvent)752 void messageFromNative(A2dpStackEvent stackEvent) { 753 Objects.requireNonNull(stackEvent.device, 754 "Device should never be null, event: " + stackEvent); 755 synchronized (mStateMachines) { 756 BluetoothDevice device = stackEvent.device; 757 A2dpStateMachine sm = mStateMachines.get(device); 758 if (sm == null) { 759 if (stackEvent.type == A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED) { 760 switch (stackEvent.valueInt) { 761 case A2dpStackEvent.CONNECTION_STATE_CONNECTED: 762 case A2dpStackEvent.CONNECTION_STATE_CONNECTING: 763 // Create a new state machine only when connecting to a device 764 if (!connectionAllowedCheckMaxDevices(device)) { 765 Log.e(TAG, "Cannot connect to " + device 766 + " : too many connected devices"); 767 return; 768 } 769 sm = getOrCreateStateMachine(device); 770 break; 771 default: 772 break; 773 } 774 } 775 } 776 if (sm == null) { 777 Log.e(TAG, "Cannot process stack event: no state machine: " + stackEvent); 778 return; 779 } 780 sm.sendMessage(A2dpStateMachine.STACK_EVENT, stackEvent); 781 } 782 } 783 784 /** 785 * The codec configuration for a device has been updated. 786 * 787 * @param device the remote device 788 * @param codecStatus the new codec status 789 * @param sameAudioFeedingParameters if true the audio feeding parameters 790 * haven't been changed 791 */ codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus, boolean sameAudioFeedingParameters)792 void codecConfigUpdated(BluetoothDevice device, BluetoothCodecStatus codecStatus, 793 boolean sameAudioFeedingParameters) { 794 broadcastCodecConfig(device, codecStatus); 795 796 // Inform the Audio Service about the codec configuration change, 797 // so the Audio Service can reset accordingly the audio feeding 798 // parameters in the Audio HAL to the Bluetooth stack. 799 if (isActiveDevice(device) && !sameAudioFeedingParameters) { 800 mAudioManager.handleBluetoothA2dpDeviceConfigChange(device); 801 } 802 } 803 getOrCreateStateMachine(BluetoothDevice device)804 private A2dpStateMachine getOrCreateStateMachine(BluetoothDevice device) { 805 if (device == null) { 806 Log.e(TAG, "getOrCreateStateMachine failed: device cannot be null"); 807 return null; 808 } 809 synchronized (mStateMachines) { 810 A2dpStateMachine sm = mStateMachines.get(device); 811 if (sm != null) { 812 return sm; 813 } 814 // Limit the maximum number of state machines to avoid DoS attack 815 if (mStateMachines.size() >= MAX_A2DP_STATE_MACHINES) { 816 Log.e(TAG, "Maximum number of A2DP state machines reached: " 817 + MAX_A2DP_STATE_MACHINES); 818 return null; 819 } 820 if (DBG) { 821 Log.d(TAG, "Creating a new state machine for " + device); 822 } 823 sm = A2dpStateMachine.make(device, this, mA2dpNativeInterface, 824 mStateMachinesThread.getLooper()); 825 mStateMachines.put(device, sm); 826 return sm; 827 } 828 } 829 broadcastActiveDevice(BluetoothDevice device)830 private void broadcastActiveDevice(BluetoothDevice device) { 831 if (DBG) { 832 Log.d(TAG, "broadcastActiveDevice(" + device + ")"); 833 } 834 835 Intent intent = new Intent(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); 836 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 837 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 838 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 839 sendBroadcast(intent, ProfileService.BLUETOOTH_PERM); 840 } 841 broadcastCodecConfig(BluetoothDevice device, BluetoothCodecStatus codecStatus)842 private void broadcastCodecConfig(BluetoothDevice device, BluetoothCodecStatus codecStatus) { 843 if (DBG) { 844 Log.d(TAG, "broadcastCodecConfig(" + device + "): " + codecStatus); 845 } 846 Intent intent = new Intent(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED); 847 intent.putExtra(BluetoothCodecStatus.EXTRA_CODEC_STATUS, codecStatus); 848 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 849 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT 850 | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND); 851 sendBroadcast(intent, A2dpService.BLUETOOTH_PERM); 852 } 853 854 private class BondStateChangedReceiver extends BroadcastReceiver { 855 @Override onReceive(Context context, Intent intent)856 public void onReceive(Context context, Intent intent) { 857 if (!BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(intent.getAction())) { 858 return; 859 } 860 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 861 BluetoothDevice.ERROR); 862 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 863 Objects.requireNonNull(device, "ACTION_BOND_STATE_CHANGED with no EXTRA_DEVICE"); 864 bondStateChanged(device, state); 865 } 866 } 867 868 /** 869 * Process a change in the bonding state for a device. 870 * 871 * @param device the device whose bonding state has changed 872 * @param bondState the new bond state for the device. Possible values are: 873 * {@link BluetoothDevice#BOND_NONE}, 874 * {@link BluetoothDevice#BOND_BONDING}, 875 * {@link BluetoothDevice#BOND_BONDED}. 876 */ 877 @VisibleForTesting bondStateChanged(BluetoothDevice device, int bondState)878 void bondStateChanged(BluetoothDevice device, int bondState) { 879 if (DBG) { 880 Log.d(TAG, "Bond state changed for device: " + device + " state: " + bondState); 881 } 882 // Remove state machine if the bonding for a device is removed 883 if (bondState != BluetoothDevice.BOND_NONE) { 884 return; 885 } 886 synchronized (mStateMachines) { 887 A2dpStateMachine sm = mStateMachines.get(device); 888 if (sm == null) { 889 return; 890 } 891 if (sm.getConnectionState() != BluetoothProfile.STATE_DISCONNECTED) { 892 return; 893 } 894 removeStateMachine(device); 895 } 896 } 897 removeStateMachine(BluetoothDevice device)898 private void removeStateMachine(BluetoothDevice device) { 899 synchronized (mStateMachines) { 900 A2dpStateMachine sm = mStateMachines.get(device); 901 if (sm == null) { 902 Log.w(TAG, "removeStateMachine: device " + device 903 + " does not have a state machine"); 904 return; 905 } 906 Log.i(TAG, "removeStateMachine: removing state machine for device: " + device); 907 sm.doQuit(); 908 sm.cleanup(); 909 mStateMachines.remove(device); 910 } 911 } 912 updateOptionalCodecsSupport(BluetoothDevice device)913 private void updateOptionalCodecsSupport(BluetoothDevice device) { 914 int previousSupport = getSupportsOptionalCodecs(device); 915 boolean supportsOptional = false; 916 917 synchronized (mStateMachines) { 918 A2dpStateMachine sm = mStateMachines.get(device); 919 if (sm == null) { 920 return; 921 } 922 BluetoothCodecStatus codecStatus = sm.getCodecStatus(); 923 if (codecStatus != null) { 924 for (BluetoothCodecConfig config : codecStatus.getCodecsSelectableCapabilities()) { 925 if (!config.isMandatoryCodec()) { 926 supportsOptional = true; 927 break; 928 } 929 } 930 } 931 } 932 if (previousSupport == BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN 933 || supportsOptional != (previousSupport 934 == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED)) { 935 setSupportsOptionalCodecs(device, supportsOptional); 936 } 937 if (supportsOptional) { 938 int enabled = getOptionalCodecsEnabled(device); 939 if (enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED) { 940 enableOptionalCodecs(device); 941 } else if (enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED) { 942 disableOptionalCodecs(device); 943 } 944 } 945 } 946 connectionStateChanged(BluetoothDevice device, int fromState, int toState)947 private void connectionStateChanged(BluetoothDevice device, int fromState, int toState) { 948 if ((device == null) || (fromState == toState)) { 949 return; 950 } 951 synchronized (mStateMachines) { 952 if (toState == BluetoothProfile.STATE_CONNECTED) { 953 // Each time a device connects, we want to re-check if it supports optional 954 // codecs (perhaps it's had a firmware update, etc.) and save that state if 955 // it differs from what we had saved before. 956 updateOptionalCodecsSupport(device); 957 MetricsLogger.logProfileConnectionEvent(BluetoothMetricsProto.ProfileId.A2DP); 958 } 959 // Set the active device if only one connected device is supported and it was connected 960 if (toState == BluetoothProfile.STATE_CONNECTED && (mMaxConnectedAudioDevices == 1)) { 961 setActiveDevice(device); 962 } 963 // Check if the active device is not connected anymore 964 if (isActiveDevice(device) && (fromState == BluetoothProfile.STATE_CONNECTED)) { 965 setActiveDevice(null); 966 } 967 // Check if the device is disconnected - if unbond, remove the state machine 968 if (toState == BluetoothProfile.STATE_DISCONNECTED) { 969 int bondState = mAdapterService.getBondState(device); 970 if (bondState == BluetoothDevice.BOND_NONE) { 971 removeStateMachine(device); 972 } 973 } 974 } 975 } 976 977 /** 978 * Receiver for processing device connection state changes. 979 * 980 * <ul> 981 * <li> Update codec support per device when device is (re)connected 982 * <li> Delete the state machine instance if the device is disconnected and unbond 983 * </ul> 984 */ 985 private class ConnectionStateChangedReceiver extends BroadcastReceiver { 986 @Override onReceive(Context context, Intent intent)987 public void onReceive(Context context, Intent intent) { 988 if (!BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) { 989 return; 990 } 991 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 992 int toState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 993 int fromState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1); 994 connectionStateChanged(device, fromState, toState); 995 } 996 } 997 998 /** 999 * Binder object: must be a static class or memory leak may occur. 1000 */ 1001 @VisibleForTesting 1002 static class BluetoothA2dpBinder extends IBluetoothA2dp.Stub 1003 implements IProfileServiceBinder { 1004 private A2dpService mService; 1005 getService()1006 private A2dpService getService() { 1007 if (!Utils.checkCaller()) { 1008 Log.w(TAG, "A2DP call not allowed for non-active user"); 1009 return null; 1010 } 1011 1012 if (mService != null && mService.isAvailable()) { 1013 return mService; 1014 } 1015 return null; 1016 } 1017 BluetoothA2dpBinder(A2dpService svc)1018 BluetoothA2dpBinder(A2dpService svc) { 1019 mService = svc; 1020 } 1021 1022 @Override cleanup()1023 public void cleanup() { 1024 mService = null; 1025 } 1026 1027 @Override connect(BluetoothDevice device)1028 public boolean connect(BluetoothDevice device) { 1029 A2dpService service = getService(); 1030 if (service == null) { 1031 return false; 1032 } 1033 return service.connect(device); 1034 } 1035 1036 @Override disconnect(BluetoothDevice device)1037 public boolean disconnect(BluetoothDevice device) { 1038 A2dpService service = getService(); 1039 if (service == null) { 1040 return false; 1041 } 1042 return service.disconnect(device); 1043 } 1044 1045 @Override getConnectedDevices()1046 public List<BluetoothDevice> getConnectedDevices() { 1047 A2dpService service = getService(); 1048 if (service == null) { 1049 return new ArrayList<>(0); 1050 } 1051 return service.getConnectedDevices(); 1052 } 1053 1054 @Override getDevicesMatchingConnectionStates(int[] states)1055 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 1056 A2dpService service = getService(); 1057 if (service == null) { 1058 return new ArrayList<>(0); 1059 } 1060 return service.getDevicesMatchingConnectionStates(states); 1061 } 1062 1063 @Override getConnectionState(BluetoothDevice device)1064 public int getConnectionState(BluetoothDevice device) { 1065 A2dpService service = getService(); 1066 if (service == null) { 1067 return BluetoothProfile.STATE_DISCONNECTED; 1068 } 1069 return service.getConnectionState(device); 1070 } 1071 1072 @Override setActiveDevice(BluetoothDevice device)1073 public boolean setActiveDevice(BluetoothDevice device) { 1074 A2dpService service = getService(); 1075 if (service == null) { 1076 return false; 1077 } 1078 return service.setActiveDevice(device); 1079 } 1080 1081 @Override getActiveDevice()1082 public BluetoothDevice getActiveDevice() { 1083 A2dpService service = getService(); 1084 if (service == null) { 1085 return null; 1086 } 1087 return service.getActiveDevice(); 1088 } 1089 1090 @Override setPriority(BluetoothDevice device, int priority)1091 public boolean setPriority(BluetoothDevice device, int priority) { 1092 A2dpService service = getService(); 1093 if (service == null) { 1094 return false; 1095 } 1096 return service.setPriority(device, priority); 1097 } 1098 1099 @Override getPriority(BluetoothDevice device)1100 public int getPriority(BluetoothDevice device) { 1101 A2dpService service = getService(); 1102 if (service == null) { 1103 return BluetoothProfile.PRIORITY_UNDEFINED; 1104 } 1105 return service.getPriority(device); 1106 } 1107 1108 @Override isAvrcpAbsoluteVolumeSupported()1109 public boolean isAvrcpAbsoluteVolumeSupported() { 1110 A2dpService service = getService(); 1111 if (service == null) { 1112 return false; 1113 } 1114 return service.isAvrcpAbsoluteVolumeSupported(); 1115 } 1116 1117 @Override setAvrcpAbsoluteVolume(int volume)1118 public void setAvrcpAbsoluteVolume(int volume) { 1119 A2dpService service = getService(); 1120 if (service == null) { 1121 return; 1122 } 1123 service.setAvrcpAbsoluteVolume(volume); 1124 } 1125 1126 @Override isA2dpPlaying(BluetoothDevice device)1127 public boolean isA2dpPlaying(BluetoothDevice device) { 1128 A2dpService service = getService(); 1129 if (service == null) { 1130 return false; 1131 } 1132 return service.isA2dpPlaying(device); 1133 } 1134 1135 @Override getCodecStatus(BluetoothDevice device)1136 public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) { 1137 A2dpService service = getService(); 1138 if (service == null) { 1139 return null; 1140 } 1141 return service.getCodecStatus(device); 1142 } 1143 1144 @Override setCodecConfigPreference(BluetoothDevice device, BluetoothCodecConfig codecConfig)1145 public void setCodecConfigPreference(BluetoothDevice device, 1146 BluetoothCodecConfig codecConfig) { 1147 A2dpService service = getService(); 1148 if (service == null) { 1149 return; 1150 } 1151 service.setCodecConfigPreference(device, codecConfig); 1152 } 1153 1154 @Override enableOptionalCodecs(BluetoothDevice device)1155 public void enableOptionalCodecs(BluetoothDevice device) { 1156 A2dpService service = getService(); 1157 if (service == null) { 1158 return; 1159 } 1160 service.enableOptionalCodecs(device); 1161 } 1162 1163 @Override disableOptionalCodecs(BluetoothDevice device)1164 public void disableOptionalCodecs(BluetoothDevice device) { 1165 A2dpService service = getService(); 1166 if (service == null) { 1167 return; 1168 } 1169 service.disableOptionalCodecs(device); 1170 } 1171 supportsOptionalCodecs(BluetoothDevice device)1172 public int supportsOptionalCodecs(BluetoothDevice device) { 1173 A2dpService service = getService(); 1174 if (service == null) { 1175 return BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN; 1176 } 1177 return service.getSupportsOptionalCodecs(device); 1178 } 1179 getOptionalCodecsEnabled(BluetoothDevice device)1180 public int getOptionalCodecsEnabled(BluetoothDevice device) { 1181 A2dpService service = getService(); 1182 if (service == null) { 1183 return BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN; 1184 } 1185 return service.getOptionalCodecsEnabled(device); 1186 } 1187 setOptionalCodecsEnabled(BluetoothDevice device, int value)1188 public void setOptionalCodecsEnabled(BluetoothDevice device, int value) { 1189 A2dpService service = getService(); 1190 if (service == null) { 1191 return; 1192 } 1193 service.setOptionalCodecsEnabled(device, value); 1194 } 1195 } 1196 1197 @Override dump(StringBuilder sb)1198 public void dump(StringBuilder sb) { 1199 super.dump(sb); 1200 ProfileService.println(sb, "mActiveDevice: " + mActiveDevice); 1201 for (A2dpStateMachine sm : mStateMachines.values()) { 1202 sm.dump(sb); 1203 } 1204 if (mAvrcp != null) { 1205 mAvrcp.dump(sb); 1206 } 1207 } 1208 } 1209