1 /* 2 * Copyright (C) 2014 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 package com.android.bluetooth.a2dpsink; 17 18 import android.annotation.RequiresPermission; 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothAudioConfig; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothProfile; 23 import android.bluetooth.IBluetoothA2dpSink; 24 import android.content.AttributionSource; 25 import android.media.AudioManager; 26 import android.sysprop.BluetoothProperties; 27 import android.util.Log; 28 29 import com.android.bluetooth.Utils; 30 import com.android.bluetooth.btservice.AdapterService; 31 import com.android.bluetooth.btservice.ProfileService; 32 import com.android.bluetooth.btservice.storage.DatabaseManager; 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.modules.utils.SynchronousResultReceiver; 35 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.List; 39 import java.util.Map; 40 import java.util.Objects; 41 import java.util.concurrent.ConcurrentHashMap; 42 43 /** 44 * Provides Bluetooth A2DP Sink profile, as a service in the Bluetooth application. 45 * @hide 46 */ 47 public class A2dpSinkService extends ProfileService { 48 private static final String TAG = "A2dpSinkService"; 49 private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 50 private int mMaxConnectedAudioDevices; 51 52 private AdapterService mAdapterService; 53 private DatabaseManager mDatabaseManager; 54 private Map<BluetoothDevice, A2dpSinkStateMachine> mDeviceStateMap = 55 new ConcurrentHashMap<>(1); 56 57 private final Object mStreamHandlerLock = new Object(); 58 59 private final Object mActiveDeviceLock = new Object(); 60 private BluetoothDevice mActiveDevice = null; 61 62 private A2dpSinkStreamHandler mA2dpSinkStreamHandler; 63 private static A2dpSinkService sService; 64 65 A2dpSinkNativeInterface mNativeInterface; 66 isEnabled()67 public static boolean isEnabled() { 68 return BluetoothProperties.isProfileA2dpSinkEnabled().orElse(false); 69 } 70 71 @Override start()72 protected boolean start() { 73 mAdapterService = Objects.requireNonNull(AdapterService.getAdapterService(), 74 "AdapterService cannot be null when A2dpSinkService starts"); 75 mDatabaseManager = Objects.requireNonNull(AdapterService.getAdapterService().getDatabase(), 76 "DatabaseManager cannot be null when A2dpSinkService starts"); 77 mNativeInterface = A2dpSinkNativeInterface.getInstance(); 78 79 mMaxConnectedAudioDevices = mAdapterService.getMaxConnectedAudioDevices(); 80 mNativeInterface.init(mMaxConnectedAudioDevices); 81 82 synchronized (mStreamHandlerLock) { 83 mA2dpSinkStreamHandler = new A2dpSinkStreamHandler(this, mNativeInterface); 84 } 85 86 setA2dpSinkService(this); 87 BluetoothDevice activeDevice = getActiveDevice(); 88 String deviceAddress = activeDevice != null ? 89 activeDevice.getAddress() : 90 AdapterService.ACTIVITY_ATTRIBUTION_NO_ACTIVE_DEVICE_ADDRESS; 91 mAdapterService.notifyActivityAttributionInfo(getAttributionSource(), deviceAddress); 92 return true; 93 } 94 95 @Override stop()96 protected boolean stop() { 97 BluetoothDevice activeDevice = getActiveDevice(); 98 String deviceAddress = activeDevice != null ? 99 activeDevice.getAddress() : 100 AdapterService.ACTIVITY_ATTRIBUTION_NO_ACTIVE_DEVICE_ADDRESS; 101 mAdapterService.notifyActivityAttributionInfo(getAttributionSource(), deviceAddress); 102 setA2dpSinkService(null); 103 mNativeInterface.cleanup(); 104 for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) { 105 stateMachine.quitNow(); 106 } 107 mDeviceStateMap.clear(); 108 synchronized (mStreamHandlerLock) { 109 if (mA2dpSinkStreamHandler != null) { 110 mA2dpSinkStreamHandler.cleanup(); 111 mA2dpSinkStreamHandler = null; 112 } 113 } 114 return true; 115 } 116 getA2dpSinkService()117 public static synchronized A2dpSinkService getA2dpSinkService() { 118 return sService; 119 } 120 121 /** 122 * Testing API to inject a mockA2dpSinkService. 123 * @hide 124 */ 125 @VisibleForTesting setA2dpSinkService(A2dpSinkService service)126 public static synchronized void setA2dpSinkService(A2dpSinkService service) { 127 sService = service; 128 } 129 130 A2dpSinkService()131 public A2dpSinkService() {} 132 133 /** 134 * Set the device that should be allowed to actively stream 135 */ setActiveDevice(BluetoothDevice device)136 public boolean setActiveDevice(BluetoothDevice device) { 137 synchronized (mActiveDeviceLock) { 138 if (mNativeInterface.setActiveDevice(device)) { 139 mActiveDevice = device; 140 return true; 141 } 142 return false; 143 } 144 } 145 146 /** 147 * Get the device that is allowed to be actively streaming 148 */ getActiveDevice()149 public BluetoothDevice getActiveDevice() { 150 synchronized (mActiveDeviceLock) { 151 return mActiveDevice; 152 } 153 } 154 155 /** 156 * Request audio focus such that the designated device can stream audio 157 */ requestAudioFocus(BluetoothDevice device, boolean request)158 public void requestAudioFocus(BluetoothDevice device, boolean request) { 159 synchronized (mStreamHandlerLock) { 160 if (mA2dpSinkStreamHandler == null) return; 161 mA2dpSinkStreamHandler.requestAudioFocus(request); 162 } 163 } 164 165 /** 166 * Get the current Bluetooth Audio focus state 167 * 168 * @return AudioManger.AUDIOFOCUS_* states on success, or AudioManager.ERROR on error 169 */ getFocusState()170 public int getFocusState() { 171 synchronized (mStreamHandlerLock) { 172 if (mA2dpSinkStreamHandler == null) return AudioManager.ERROR; 173 return mA2dpSinkStreamHandler.getFocusState(); 174 } 175 } 176 177 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) isA2dpPlaying(BluetoothDevice device)178 boolean isA2dpPlaying(BluetoothDevice device) { 179 enforceCallingOrSelfPermission( 180 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 181 synchronized (mStreamHandlerLock) { 182 if (mA2dpSinkStreamHandler == null) return false; 183 return mA2dpSinkStreamHandler.isPlaying(); 184 } 185 } 186 187 @Override initBinder()188 protected IProfileServiceBinder initBinder() { 189 return new A2dpSinkServiceBinder(this); 190 } 191 192 //Binder object: Must be static class or memory leak may occur 193 @VisibleForTesting 194 static class A2dpSinkServiceBinder extends IBluetoothA2dpSink.Stub 195 implements IProfileServiceBinder { 196 private A2dpSinkService mService; 197 198 @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) getService(AttributionSource source)199 private A2dpSinkService getService(AttributionSource source) { 200 if (Utils.isInstrumentationTestMode()) { 201 return mService; 202 } 203 if (!Utils.checkServiceAvailable(mService, TAG) 204 || !Utils.checkCallerIsSystemOrActiveOrManagedUser(mService, TAG) 205 || !Utils.checkConnectPermissionForDataDelivery(mService, source, TAG)) { 206 return null; 207 } 208 return mService; 209 } 210 A2dpSinkServiceBinder(A2dpSinkService svc)211 A2dpSinkServiceBinder(A2dpSinkService svc) { 212 mService = svc; 213 } 214 215 @Override cleanup()216 public void cleanup() { 217 mService = null; 218 } 219 220 @Override connect(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)221 public void connect(BluetoothDevice device, AttributionSource source, 222 SynchronousResultReceiver receiver) { 223 try { 224 A2dpSinkService service = getService(source); 225 boolean result = false; 226 if (service != null) { 227 result = service.connect(device); 228 } 229 receiver.send(result); 230 } catch (RuntimeException e) { 231 receiver.propagateException(e); 232 } 233 } 234 235 @Override disconnect(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)236 public void disconnect(BluetoothDevice device, AttributionSource source, 237 SynchronousResultReceiver receiver) { 238 try { 239 A2dpSinkService service = getService(source); 240 boolean result = false; 241 if (service != null) { 242 result = service.disconnect(device); 243 } 244 receiver.send(result); 245 } catch (RuntimeException e) { 246 receiver.propagateException(e); 247 } 248 } 249 250 @Override getConnectedDevices(AttributionSource source, SynchronousResultReceiver receiver)251 public void getConnectedDevices(AttributionSource source, 252 SynchronousResultReceiver receiver) { 253 try { 254 A2dpSinkService service = getService(source); 255 List<BluetoothDevice> result = new ArrayList<BluetoothDevice>(0); 256 if (service != null) { 257 result = service.getConnectedDevices(); 258 } 259 receiver.send(result); 260 } catch (RuntimeException e) { 261 receiver.propagateException(e); 262 } 263 } 264 265 @Override getDevicesMatchingConnectionStates(int[] states, AttributionSource source, SynchronousResultReceiver receiver)266 public void getDevicesMatchingConnectionStates(int[] states, 267 AttributionSource source, SynchronousResultReceiver receiver) { 268 try { 269 A2dpSinkService service = getService(source); 270 List<BluetoothDevice> result = new ArrayList<BluetoothDevice>(0); 271 if (service != null) { 272 result = service.getDevicesMatchingConnectionStates(states); 273 } 274 receiver.send(result); 275 } catch (RuntimeException e) { 276 receiver.propagateException(e); 277 } 278 } 279 280 @Override getConnectionState(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)281 public void getConnectionState(BluetoothDevice device, AttributionSource source, 282 SynchronousResultReceiver receiver) { 283 try { 284 A2dpSinkService service = getService(source); 285 int result = BluetoothProfile.STATE_DISCONNECTED; 286 if (service != null) { 287 result = service.getConnectionState(device); 288 } 289 receiver.send(result); 290 } catch (RuntimeException e) { 291 receiver.propagateException(e); 292 } 293 } 294 295 @Override setConnectionPolicy(BluetoothDevice device, int connectionPolicy, AttributionSource source, SynchronousResultReceiver receiver)296 public void setConnectionPolicy(BluetoothDevice device, int connectionPolicy, 297 AttributionSource source, SynchronousResultReceiver receiver) { 298 try { 299 A2dpSinkService service = getService(source); 300 boolean result = false; 301 if (service != null) { 302 result = service.setConnectionPolicy(device, connectionPolicy); 303 } 304 receiver.send(result); 305 } catch (RuntimeException e) { 306 receiver.propagateException(e); 307 } 308 } 309 310 @Override getConnectionPolicy(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)311 public void getConnectionPolicy(BluetoothDevice device, AttributionSource source, 312 SynchronousResultReceiver receiver) { 313 try { 314 A2dpSinkService service = getService(source); 315 int result = BluetoothProfile.CONNECTION_POLICY_UNKNOWN; 316 if (service != null) { 317 result = service.getConnectionPolicy(device); 318 } 319 receiver.send(result); 320 } catch (RuntimeException e) { 321 receiver.propagateException(e); 322 } 323 } 324 325 @Override isA2dpPlaying(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)326 public void isA2dpPlaying(BluetoothDevice device, AttributionSource source, 327 SynchronousResultReceiver receiver) { 328 try { 329 A2dpSinkService service = getService(source); 330 boolean result = false; 331 if (service != null) { 332 result = service.isA2dpPlaying(device); 333 } 334 receiver.send(result); 335 } catch (RuntimeException e) { 336 receiver.propagateException(e); 337 } 338 } 339 340 @Override getAudioConfig(BluetoothDevice device, AttributionSource source, SynchronousResultReceiver receiver)341 public void getAudioConfig(BluetoothDevice device, 342 AttributionSource source, SynchronousResultReceiver receiver) { 343 try { 344 A2dpSinkService service = getService(source); 345 BluetoothAudioConfig result = null; 346 if (service != null) { 347 result = service.getAudioConfig(device); 348 } 349 receiver.send(result); 350 } catch (RuntimeException e) { 351 receiver.propagateException(e); 352 } 353 } 354 } 355 356 /* Generic Profile Code */ 357 358 /** 359 * Connect the given Bluetooth device. 360 * 361 * @return true if connection is successful, false otherwise. 362 */ 363 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) connect(BluetoothDevice device)364 public boolean connect(BluetoothDevice device) { 365 enforceCallingOrSelfPermission(BLUETOOTH_PRIVILEGED, 366 "Need BLUETOOTH_PRIVILEGED permission"); 367 if (device == null) { 368 throw new IllegalArgumentException("Null device"); 369 } 370 if (DBG) { 371 StringBuilder sb = new StringBuilder(); 372 dump(sb); 373 Log.d(TAG, " connect device: " + device 374 + ", InstanceMap start state: " + sb.toString()); 375 } 376 if (getConnectionPolicy(device) == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 377 Log.w(TAG, "Connection not allowed: <" + device 378 + "> is CONNECTION_POLICY_FORBIDDEN"); 379 return false; 380 } 381 382 A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device); 383 if (stateMachine != null) { 384 stateMachine.connect(); 385 return true; 386 } else { 387 // a state machine instance doesn't exist yet, and the max has been reached. 388 Log.e(TAG, "Maxed out on the number of allowed A2DP Sink connections. " 389 + "Connect request rejected on " + device); 390 return false; 391 } 392 } 393 394 /** 395 * Disconnect the given Bluetooth device. 396 * 397 * @return true if disconnect is successful, false otherwise. 398 */ disconnect(BluetoothDevice device)399 public boolean disconnect(BluetoothDevice device) { 400 if (DBG) { 401 StringBuilder sb = new StringBuilder(); 402 dump(sb); 403 Log.d(TAG, "A2DP disconnect device: " + device 404 + ", InstanceMap start state: " + sb.toString()); 405 } 406 407 if (device == null) { 408 throw new IllegalArgumentException("Null device"); 409 } 410 411 A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); 412 // a state machine instance doesn't exist. maybe it is already gone? 413 if (stateMachine == null) { 414 return false; 415 } 416 int connectionState = stateMachine.getState(); 417 if (connectionState == BluetoothProfile.STATE_DISCONNECTED 418 || connectionState == BluetoothProfile.STATE_DISCONNECTING) { 419 return false; 420 } 421 // upon completion of disconnect, the state machine will remove itself from the available 422 // devices map 423 stateMachine.disconnect(); 424 return true; 425 } 426 427 /** 428 * Remove a device's state machine. 429 * 430 * Called by the state machines when they disconnect. 431 * 432 * Visible for testing so it can be mocked and verified on. 433 */ 434 @VisibleForTesting removeStateMachine(A2dpSinkStateMachine stateMachine)435 public void removeStateMachine(A2dpSinkStateMachine stateMachine) { 436 mDeviceStateMap.remove(stateMachine.getDevice()); 437 } 438 getConnectedDevices()439 public List<BluetoothDevice> getConnectedDevices() { 440 return getDevicesMatchingConnectionStates(new int[]{BluetoothAdapter.STATE_CONNECTED}); 441 } 442 getOrCreateStateMachine(BluetoothDevice device)443 protected A2dpSinkStateMachine getOrCreateStateMachine(BluetoothDevice device) { 444 A2dpSinkStateMachine newStateMachine = 445 new A2dpSinkStateMachine(device, this, mNativeInterface); 446 A2dpSinkStateMachine existingStateMachine = 447 mDeviceStateMap.putIfAbsent(device, newStateMachine); 448 // Given null is not a valid value in our map, ConcurrentHashMap will return null if the 449 // key was absent and our new value was added. We should then start and return it. 450 if (existingStateMachine == null) { 451 newStateMachine.start(); 452 return newStateMachine; 453 } 454 return existingStateMachine; 455 } 456 457 @VisibleForTesting getStateMachineForDevice(BluetoothDevice device)458 protected A2dpSinkStateMachine getStateMachineForDevice(BluetoothDevice device) { 459 return mDeviceStateMap.get(device); 460 } 461 getDevicesMatchingConnectionStates(int[] states)462 List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 463 if (DBG) Log.d(TAG, "getDevicesMatchingConnectionStates" + Arrays.toString(states)); 464 List<BluetoothDevice> deviceList = new ArrayList<>(); 465 BluetoothDevice[] bondedDevices = mAdapterService.getBondedDevices(); 466 int connectionState; 467 for (BluetoothDevice device : bondedDevices) { 468 connectionState = getConnectionState(device); 469 if (DBG) Log.d(TAG, "Device: " + device + "State: " + connectionState); 470 for (int i = 0; i < states.length; i++) { 471 if (connectionState == states[i]) { 472 deviceList.add(device); 473 } 474 } 475 } 476 if (DBG) Log.d(TAG, deviceList.toString()); 477 Log.d(TAG, "GetDevicesDone"); 478 return deviceList; 479 } 480 481 /** 482 * Get the current connection state of the profile 483 * 484 * @param device is the remote bluetooth device 485 * @return {@link BluetoothProfile#STATE_DISCONNECTED} if this profile is disconnected, 486 * {@link BluetoothProfile#STATE_CONNECTING} if this profile is being connected, 487 * {@link BluetoothProfile#STATE_CONNECTED} if this profile is connected, or 488 * {@link BluetoothProfile#STATE_DISCONNECTING} if this profile is being disconnected 489 */ getConnectionState(BluetoothDevice device)490 public int getConnectionState(BluetoothDevice device) { 491 if (device == null) return BluetoothProfile.STATE_DISCONNECTED; 492 A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); 493 return (stateMachine == null) ? BluetoothProfile.STATE_DISCONNECTED 494 : stateMachine.getState(); 495 } 496 497 /** 498 * Set connection policy of the profile and connects it if connectionPolicy is 499 * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED} or disconnects if connectionPolicy is 500 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN} 501 * 502 * <p> The device should already be paired. 503 * Connection policy can be one of: 504 * {@link BluetoothProfile#CONNECTION_POLICY_ALLOWED}, 505 * {@link BluetoothProfile#CONNECTION_POLICY_FORBIDDEN}, 506 * {@link BluetoothProfile#CONNECTION_POLICY_UNKNOWN} 507 * 508 * @param device Paired bluetooth device 509 * @param connectionPolicy is the connection policy to set to for this profile 510 * @return true if connectionPolicy is set, false on error 511 */ 512 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) setConnectionPolicy(BluetoothDevice device, int connectionPolicy)513 public boolean setConnectionPolicy(BluetoothDevice device, int connectionPolicy) { 514 enforceCallingOrSelfPermission( 515 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 516 if (DBG) { 517 Log.d(TAG, "Saved connectionPolicy " + device + " = " + connectionPolicy); 518 } 519 520 if (!mDatabaseManager.setProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK, 521 connectionPolicy)) { 522 return false; 523 } 524 if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { 525 connect(device); 526 } else if (connectionPolicy == BluetoothProfile.CONNECTION_POLICY_FORBIDDEN) { 527 disconnect(device); 528 } 529 return true; 530 } 531 532 /** 533 * Get the connection policy of the profile. 534 * 535 * @param device the remote device 536 * @return connection policy of the specified device 537 */ 538 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) getConnectionPolicy(BluetoothDevice device)539 public int getConnectionPolicy(BluetoothDevice device) { 540 enforceCallingOrSelfPermission( 541 BLUETOOTH_PRIVILEGED, "Need BLUETOOTH_PRIVILEGED permission"); 542 return mDatabaseManager 543 .getProfileConnectionPolicy(device, BluetoothProfile.A2DP_SINK); 544 } 545 546 547 @Override dump(StringBuilder sb)548 public void dump(StringBuilder sb) { 549 super.dump(sb); 550 ProfileService.println(sb, "Active Device = " + getActiveDevice()); 551 ProfileService.println(sb, "Max Connected Devices = " + mMaxConnectedAudioDevices); 552 ProfileService.println(sb, "Devices Tracked = " + mDeviceStateMap.size()); 553 for (A2dpSinkStateMachine stateMachine : mDeviceStateMap.values()) { 554 ProfileService.println(sb, 555 "==== StateMachine for " + stateMachine.getDevice() + " ===="); 556 stateMachine.dump(sb); 557 } 558 } 559 getAudioConfig(BluetoothDevice device)560 BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { 561 if (device == null) return null; 562 A2dpSinkStateMachine stateMachine = mDeviceStateMap.get(device); 563 // a state machine instance doesn't exist. maybe it is already gone? 564 if (stateMachine == null) { 565 return null; 566 } 567 return stateMachine.getAudioConfig(); 568 } 569 570 /** 571 * Receive and route a stack event from the JNI 572 */ messageFromNative(StackEvent event)573 protected void messageFromNative(StackEvent event) { 574 switch (event.mType) { 575 case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: 576 onConnectionStateChanged(event); 577 return; 578 case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED: 579 onAudioStateChanged(event); 580 return; 581 case StackEvent.EVENT_TYPE_AUDIO_CONFIG_CHANGED: 582 onAudioConfigChanged(event); 583 return; 584 default: 585 Log.e(TAG, "Received unknown stack event of type " + event.mType); 586 return; 587 } 588 } 589 onConnectionStateChanged(StackEvent event)590 private void onConnectionStateChanged(StackEvent event) { 591 BluetoothDevice device = event.mDevice; 592 if (device == null) { 593 return; 594 } 595 A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device); 596 stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event); 597 } 598 onAudioStateChanged(StackEvent event)599 private void onAudioStateChanged(StackEvent event) { 600 int state = event.mState; 601 synchronized (mStreamHandlerLock) { 602 if (mA2dpSinkStreamHandler == null) { 603 Log.e(TAG, "Received audio state change before we've been started"); 604 return; 605 } else if (state == StackEvent.AUDIO_STATE_STARTED) { 606 mA2dpSinkStreamHandler.obtainMessage( 607 A2dpSinkStreamHandler.SRC_STR_START).sendToTarget(); 608 } else if (state == StackEvent.AUDIO_STATE_STOPPED 609 || state == StackEvent.AUDIO_STATE_REMOTE_SUSPEND) { 610 mA2dpSinkStreamHandler.obtainMessage( 611 A2dpSinkStreamHandler.SRC_STR_STOP).sendToTarget(); 612 } else { 613 Log.w(TAG, "Unhandled audio state change, state=" + state); 614 } 615 } 616 } 617 onAudioConfigChanged(StackEvent event)618 private void onAudioConfigChanged(StackEvent event) { 619 BluetoothDevice device = event.mDevice; 620 if (device == null) { 621 return; 622 } 623 A2dpSinkStateMachine stateMachine = getOrCreateStateMachine(device); 624 stateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT, event); 625 } 626 } 627