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