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 17 package android.bluetooth; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.ServiceConnection; 23 import android.os.Binder; 24 import android.os.IBinder; 25 import android.os.RemoteException; 26 import android.util.Log; 27 28 import java.util.ArrayList; 29 import java.util.List; 30 31 /** 32 * This class provides the public APIs to control the Bluetooth A2DP Sink 33 * profile. 34 * 35 * <p>BluetoothA2dpSink is a proxy object for controlling the Bluetooth A2DP Sink 36 * Service via IPC. Use {@link BluetoothAdapter#getProfileProxy} to get 37 * the BluetoothA2dpSink proxy object. 38 * 39 * @hide 40 */ 41 public final class BluetoothA2dpSink implements BluetoothProfile { 42 private static final String TAG = "BluetoothA2dpSink"; 43 private static final boolean DBG = true; 44 private static final boolean VDBG = false; 45 46 /** 47 * Intent used to broadcast the change in connection state of the A2DP Sink 48 * profile. 49 * 50 * <p>This intent will have 3 extras: 51 * <ul> 52 * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> 53 * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile.</li> 54 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 55 * </ul> 56 * 57 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of 58 * {@link #STATE_DISCONNECTED}, {@link #STATE_CONNECTING}, 59 * {@link #STATE_CONNECTED}, {@link #STATE_DISCONNECTING}. 60 * 61 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to 62 * receive. 63 */ 64 public static final String ACTION_CONNECTION_STATE_CHANGED = 65 "android.bluetooth.a2dp-sink.profile.action.CONNECTION_STATE_CHANGED"; 66 67 /** 68 * Intent used to broadcast the change in the Playing state of the A2DP Sink 69 * profile. 70 * 71 * <p>This intent will have 3 extras: 72 * <ul> 73 * <li> {@link #EXTRA_STATE} - The current state of the profile. </li> 74 * <li> {@link #EXTRA_PREVIOUS_STATE}- The previous state of the profile. </li> 75 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 76 * </ul> 77 * 78 * <p>{@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} can be any of 79 * {@link #STATE_PLAYING}, {@link #STATE_NOT_PLAYING}, 80 * 81 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to 82 * receive. 83 */ 84 public static final String ACTION_PLAYING_STATE_CHANGED = 85 "android.bluetooth.a2dp-sink.profile.action.PLAYING_STATE_CHANGED"; 86 87 /** 88 * A2DP sink device is streaming music. This state can be one of 89 * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of 90 * {@link #ACTION_PLAYING_STATE_CHANGED} intent. 91 */ 92 public static final int STATE_PLAYING = 10; 93 94 /** 95 * A2DP sink device is NOT streaming music. This state can be one of 96 * {@link #EXTRA_STATE} or {@link #EXTRA_PREVIOUS_STATE} of 97 * {@link #ACTION_PLAYING_STATE_CHANGED} intent. 98 */ 99 public static final int STATE_NOT_PLAYING = 11; 100 101 /** 102 * Intent used to broadcast the change in the Playing state of the A2DP Sink 103 * profile. 104 * 105 * <p>This intent will have 3 extras: 106 * <ul> 107 * <li> {@link #EXTRA_AUDIO_CONFIG} - The audio configuration for the remote device. </li> 108 * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. </li> 109 * </ul> 110 * 111 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to 112 * receive. 113 */ 114 public static final String ACTION_AUDIO_CONFIG_CHANGED = 115 "android.bluetooth.a2dp-sink.profile.action.AUDIO_CONFIG_CHANGED"; 116 117 /** 118 * Extra for the {@link #ACTION_AUDIO_CONFIG_CHANGED} intent. 119 * 120 * This extra represents the current audio configuration of the A2DP source device. 121 * {@see BluetoothAudioConfig} 122 */ 123 public static final String EXTRA_AUDIO_CONFIG = 124 "android.bluetooth.a2dp-sink.profile.extra.AUDIO_CONFIG"; 125 126 private Context mContext; 127 private ServiceListener mServiceListener; 128 private volatile IBluetoothA2dpSink mService; 129 private BluetoothAdapter mAdapter; 130 131 private final IBluetoothStateChangeCallback mBluetoothStateChangeCallback = 132 new IBluetoothStateChangeCallback.Stub() { 133 public void onBluetoothStateChange(boolean up) { 134 if (DBG) Log.d(TAG, "onBluetoothStateChange: up=" + up); 135 if (!up) { 136 if (VDBG) Log.d(TAG, "Unbinding service..."); 137 synchronized (mConnection) { 138 try { 139 mService = null; 140 mContext.unbindService(mConnection); 141 } catch (Exception re) { 142 Log.e(TAG, "", re); 143 } 144 } 145 } else { 146 synchronized (mConnection) { 147 try { 148 if (mService == null) { 149 if (VDBG) Log.d(TAG, "Binding service..."); 150 doBind(); 151 } 152 } catch (Exception re) { 153 Log.e(TAG, "", re); 154 } 155 } 156 } 157 } 158 }; 159 160 /** 161 * Create a BluetoothA2dp proxy object for interacting with the local 162 * Bluetooth A2DP service. 163 */ 164 /*package*/ BluetoothA2dpSink(Context context, ServiceListener l) { 165 mContext = context; 166 mServiceListener = l; 167 mAdapter = BluetoothAdapter.getDefaultAdapter(); 168 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 169 if (mgr != null) { 170 try { 171 mgr.registerStateChangeCallback(mBluetoothStateChangeCallback); 172 } catch (RemoteException e) { 173 Log.e(TAG, "", e); 174 } 175 } 176 177 doBind(); 178 } 179 180 boolean doBind() { 181 Intent intent = new Intent(IBluetoothA2dpSink.class.getName()); 182 ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); 183 intent.setComponent(comp); 184 if (comp == null || !mContext.bindServiceAsUser(intent, mConnection, 0, 185 mContext.getUser())) { 186 Log.e(TAG, "Could not bind to Bluetooth A2DP Service with " + intent); 187 return false; 188 } 189 return true; 190 } 191 192 /*package*/ void close() { 193 mServiceListener = null; 194 IBluetoothManager mgr = mAdapter.getBluetoothManager(); 195 if (mgr != null) { 196 try { 197 mgr.unregisterStateChangeCallback(mBluetoothStateChangeCallback); 198 } catch (Exception e) { 199 Log.e(TAG, "", e); 200 } 201 } 202 203 synchronized (mConnection) { 204 if (mService != null) { 205 try { 206 mService = null; 207 mContext.unbindService(mConnection); 208 } catch (Exception re) { 209 Log.e(TAG, "", re); 210 } 211 } 212 } 213 } 214 215 @Override 216 public void finalize() { 217 close(); 218 } 219 220 /** 221 * Initiate connection to a profile of the remote bluetooth device. 222 * 223 * <p> Currently, the system supports only 1 connection to the 224 * A2DP profile. The API will automatically disconnect connected 225 * devices before connecting. 226 * 227 * <p> This API returns false in scenarios like the profile on the 228 * device is already connected or Bluetooth is not turned on. 229 * When this API returns true, it is guaranteed that 230 * connection state intent for the profile will be broadcasted with 231 * the state. Users can get the connection state of the profile 232 * from this intent. 233 * 234 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 235 * permission. 236 * 237 * @param device Remote Bluetooth Device 238 * @return false on immediate error, true otherwise 239 * @hide 240 */ 241 public boolean connect(BluetoothDevice device) { 242 if (DBG) log("connect(" + device + ")"); 243 final IBluetoothA2dpSink service = mService; 244 if (service != null && isEnabled() && isValidDevice(device)) { 245 try { 246 return service.connect(device); 247 } catch (RemoteException e) { 248 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 249 return false; 250 } 251 } 252 if (service == null) Log.w(TAG, "Proxy not attached to service"); 253 return false; 254 } 255 256 /** 257 * Initiate disconnection from a profile 258 * 259 * <p> This API will return false in scenarios like the profile on the 260 * Bluetooth device is not in connected state etc. When this API returns, 261 * true, it is guaranteed that the connection state change 262 * intent will be broadcasted with the state. Users can get the 263 * disconnection state of the profile from this intent. 264 * 265 * <p> If the disconnection is initiated by a remote device, the state 266 * will transition from {@link #STATE_CONNECTED} to 267 * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the 268 * host (local) device the state will transition from 269 * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to 270 * state {@link #STATE_DISCONNECTED}. The transition to 271 * {@link #STATE_DISCONNECTING} can be used to distinguish between the 272 * two scenarios. 273 * 274 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 275 * permission. 276 * 277 * @param device Remote Bluetooth Device 278 * @return false on immediate error, true otherwise 279 * @hide 280 */ 281 public boolean disconnect(BluetoothDevice device) { 282 if (DBG) log("disconnect(" + device + ")"); 283 final IBluetoothA2dpSink service = mService; 284 if (service != null && isEnabled() && isValidDevice(device)) { 285 try { 286 return service.disconnect(device); 287 } catch (RemoteException e) { 288 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 289 return false; 290 } 291 } 292 if (service == null) Log.w(TAG, "Proxy not attached to service"); 293 return false; 294 } 295 296 /** 297 * {@inheritDoc} 298 */ 299 @Override 300 public List<BluetoothDevice> getConnectedDevices() { 301 if (VDBG) log("getConnectedDevices()"); 302 final IBluetoothA2dpSink service = mService; 303 if (service != null && isEnabled()) { 304 try { 305 return service.getConnectedDevices(); 306 } catch (RemoteException e) { 307 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 308 return new ArrayList<BluetoothDevice>(); 309 } 310 } 311 if (service == null) Log.w(TAG, "Proxy not attached to service"); 312 return new ArrayList<BluetoothDevice>(); 313 } 314 315 /** 316 * {@inheritDoc} 317 */ 318 @Override 319 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 320 if (VDBG) log("getDevicesMatchingStates()"); 321 final IBluetoothA2dpSink service = mService; 322 if (service != null && isEnabled()) { 323 try { 324 return service.getDevicesMatchingConnectionStates(states); 325 } catch (RemoteException e) { 326 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 327 return new ArrayList<BluetoothDevice>(); 328 } 329 } 330 if (service == null) Log.w(TAG, "Proxy not attached to service"); 331 return new ArrayList<BluetoothDevice>(); 332 } 333 334 /** 335 * {@inheritDoc} 336 */ 337 @Override 338 public int getConnectionState(BluetoothDevice device) { 339 if (VDBG) log("getState(" + device + ")"); 340 final IBluetoothA2dpSink service = mService; 341 if (service != null && isEnabled() && isValidDevice(device)) { 342 try { 343 return service.getConnectionState(device); 344 } catch (RemoteException e) { 345 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 346 return BluetoothProfile.STATE_DISCONNECTED; 347 } 348 } 349 if (service == null) Log.w(TAG, "Proxy not attached to service"); 350 return BluetoothProfile.STATE_DISCONNECTED; 351 } 352 353 /** 354 * Get the current audio configuration for the A2DP source device, 355 * or null if the device has no audio configuration 356 * 357 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 358 * 359 * @param device Remote bluetooth device. 360 * @return audio configuration for the device, or null 361 * 362 * {@see BluetoothAudioConfig} 363 */ 364 public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { 365 if (VDBG) log("getAudioConfig(" + device + ")"); 366 final IBluetoothA2dpSink service = mService; 367 if (service != null && isEnabled() && isValidDevice(device)) { 368 try { 369 return service.getAudioConfig(device); 370 } catch (RemoteException e) { 371 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 372 return null; 373 } 374 } 375 if (service == null) Log.w(TAG, "Proxy not attached to service"); 376 return null; 377 } 378 379 /** 380 * Set priority of the profile 381 * 382 * <p> The device should already be paired. 383 * Priority can be one of {@link #PRIORITY_ON} orgetBluetoothManager 384 * {@link #PRIORITY_OFF}, 385 * 386 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 387 * permission. 388 * 389 * @param device Paired bluetooth device 390 * @param priority 391 * @return true if priority is set, false on error 392 * @hide 393 */ 394 public boolean setPriority(BluetoothDevice device, int priority) { 395 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 396 final IBluetoothA2dpSink service = mService; 397 if (service != null && isEnabled() && isValidDevice(device)) { 398 if (priority != BluetoothProfile.PRIORITY_OFF 399 && priority != BluetoothProfile.PRIORITY_ON) { 400 return false; 401 } 402 try { 403 return service.setPriority(device, priority); 404 } catch (RemoteException e) { 405 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 406 return false; 407 } 408 } 409 if (service == null) Log.w(TAG, "Proxy not attached to service"); 410 return false; 411 } 412 413 /** 414 * Get the priority of the profile. 415 * 416 * <p> The priority can be any of: 417 * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, 418 * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} 419 * 420 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 421 * 422 * @param device Bluetooth device 423 * @return priority of the device 424 * @hide 425 */ 426 public int getPriority(BluetoothDevice device) { 427 if (VDBG) log("getPriority(" + device + ")"); 428 final IBluetoothA2dpSink service = mService; 429 if (service != null && isEnabled() && isValidDevice(device)) { 430 try { 431 return service.getPriority(device); 432 } catch (RemoteException e) { 433 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 434 return BluetoothProfile.PRIORITY_OFF; 435 } 436 } 437 if (service == null) Log.w(TAG, "Proxy not attached to service"); 438 return BluetoothProfile.PRIORITY_OFF; 439 } 440 441 /** 442 * Check if A2DP profile is streaming music. 443 * 444 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 445 * 446 * @param device BluetoothDevice device 447 */ 448 public boolean isA2dpPlaying(BluetoothDevice device) { 449 final IBluetoothA2dpSink service = mService; 450 if (service != null && isEnabled() && isValidDevice(device)) { 451 try { 452 return service.isA2dpPlaying(device); 453 } catch (RemoteException e) { 454 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 455 return false; 456 } 457 } 458 if (service == null) Log.w(TAG, "Proxy not attached to service"); 459 return false; 460 } 461 462 /** 463 * Helper for converting a state to a string. 464 * 465 * For debug use only - strings are not internationalized. 466 * 467 * @hide 468 */ 469 public static String stateToString(int state) { 470 switch (state) { 471 case STATE_DISCONNECTED: 472 return "disconnected"; 473 case STATE_CONNECTING: 474 return "connecting"; 475 case STATE_CONNECTED: 476 return "connected"; 477 case STATE_DISCONNECTING: 478 return "disconnecting"; 479 case STATE_PLAYING: 480 return "playing"; 481 case STATE_NOT_PLAYING: 482 return "not playing"; 483 default: 484 return "<unknown state " + state + ">"; 485 } 486 } 487 488 private final ServiceConnection mConnection = new ServiceConnection() { 489 public void onServiceConnected(ComponentName className, IBinder service) { 490 if (DBG) Log.d(TAG, "Proxy object connected"); 491 mService = IBluetoothA2dpSink.Stub.asInterface(Binder.allowBlocking(service)); 492 if (mServiceListener != null) { 493 mServiceListener.onServiceConnected(BluetoothProfile.A2DP_SINK, 494 BluetoothA2dpSink.this); 495 } 496 } 497 498 public void onServiceDisconnected(ComponentName className) { 499 if (DBG) Log.d(TAG, "Proxy object disconnected"); 500 mService = null; 501 if (mServiceListener != null) { 502 mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP_SINK); 503 } 504 } 505 }; 506 507 private boolean isEnabled() { 508 return mAdapter.getState() == BluetoothAdapter.STATE_ON; 509 } 510 511 private static boolean isValidDevice(BluetoothDevice device) { 512 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 513 } 514 515 private static void log(String msg) { 516 Log.d(TAG, msg); 517 } 518 } 519