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