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 IBluetoothA2dpSink mService; 129 private BluetoothAdapter mAdapter; 130 131 final private 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 * Create a BluetoothA2dp proxy object for interacting with the local 161 * Bluetooth A2DP service. 162 * 163 */ BluetoothA2dpSink(Context context, ServiceListener l)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 doBind()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 android.os.Process.myUserHandle())) { 186 Log.e(TAG, "Could not bind to Bluetooth A2DP Service with " + intent); 187 return false; 188 } 189 return true; 190 } 191 close()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 finalize()215 public void finalize() { 216 close(); 217 } 218 /** 219 * Initiate connection to a profile of the remote bluetooth device. 220 * 221 * <p> Currently, the system supports only 1 connection to the 222 * A2DP profile. The API will automatically disconnect connected 223 * devices before connecting. 224 * 225 * <p> This API returns false in scenarios like the profile on the 226 * device is already connected or Bluetooth is not turned on. 227 * When this API returns true, it is guaranteed that 228 * connection state intent for the profile will be broadcasted with 229 * the state. Users can get the connection state of the profile 230 * from this intent. 231 * 232 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 233 * permission. 234 * 235 * @param device Remote Bluetooth Device 236 * @return false on immediate error, 237 * true otherwise 238 * @hide 239 */ connect(BluetoothDevice device)240 public boolean connect(BluetoothDevice device) { 241 if (DBG) log("connect(" + device + ")"); 242 if (mService != null && isEnabled() && 243 isValidDevice(device)) { 244 try { 245 return mService.connect(device); 246 } catch (RemoteException e) { 247 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 248 return false; 249 } 250 } 251 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 252 return false; 253 } 254 255 /** 256 * Initiate disconnection from a profile 257 * 258 * <p> This API will return false in scenarios like the profile on the 259 * Bluetooth device is not in connected state etc. When this API returns, 260 * true, it is guaranteed that the connection state change 261 * intent will be broadcasted with the state. Users can get the 262 * disconnection state of the profile from this intent. 263 * 264 * <p> If the disconnection is initiated by a remote device, the state 265 * will transition from {@link #STATE_CONNECTED} to 266 * {@link #STATE_DISCONNECTED}. If the disconnect is initiated by the 267 * host (local) device the state will transition from 268 * {@link #STATE_CONNECTED} to state {@link #STATE_DISCONNECTING} to 269 * state {@link #STATE_DISCONNECTED}. The transition to 270 * {@link #STATE_DISCONNECTING} can be used to distinguish between the 271 * two scenarios. 272 * 273 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 274 * permission. 275 * 276 * @param device Remote Bluetooth Device 277 * @return false on immediate error, 278 * true otherwise 279 * @hide 280 */ disconnect(BluetoothDevice device)281 public boolean disconnect(BluetoothDevice device) { 282 if (DBG) log("disconnect(" + device + ")"); 283 if (mService != null && isEnabled() && 284 isValidDevice(device)) { 285 try { 286 return mService.disconnect(device); 287 } catch (RemoteException e) { 288 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 289 return false; 290 } 291 } 292 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 293 return false; 294 } 295 296 /** 297 * {@inheritDoc} 298 */ getConnectedDevices()299 public List<BluetoothDevice> getConnectedDevices() { 300 if (VDBG) log("getConnectedDevices()"); 301 if (mService != null && isEnabled()) { 302 try { 303 return mService.getConnectedDevices(); 304 } catch (RemoteException e) { 305 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 306 return new ArrayList<BluetoothDevice>(); 307 } 308 } 309 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 310 return new ArrayList<BluetoothDevice>(); 311 } 312 313 /** 314 * {@inheritDoc} 315 */ getDevicesMatchingConnectionStates(int[] states)316 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 317 if (VDBG) log("getDevicesMatchingStates()"); 318 if (mService != null && isEnabled()) { 319 try { 320 return mService.getDevicesMatchingConnectionStates(states); 321 } catch (RemoteException e) { 322 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 323 return new ArrayList<BluetoothDevice>(); 324 } 325 } 326 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 327 return new ArrayList<BluetoothDevice>(); 328 } 329 330 /** 331 * {@inheritDoc} 332 */ getConnectionState(BluetoothDevice device)333 public int getConnectionState(BluetoothDevice device) { 334 if (VDBG) log("getState(" + device + ")"); 335 if (mService != null && isEnabled() 336 && isValidDevice(device)) { 337 try { 338 return mService.getConnectionState(device); 339 } catch (RemoteException e) { 340 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 341 return BluetoothProfile.STATE_DISCONNECTED; 342 } 343 } 344 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 345 return BluetoothProfile.STATE_DISCONNECTED; 346 } 347 348 /** 349 * Get the current audio configuration for the A2DP source device, 350 * or null if the device has no audio configuration 351 * 352 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 353 * 354 * @param device Remote bluetooth device. 355 * @return audio configuration for the device, or null 356 * 357 * {@see BluetoothAudioConfig} 358 */ getAudioConfig(BluetoothDevice device)359 public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { 360 if (VDBG) log("getAudioConfig(" + device + ")"); 361 if (mService != null && isEnabled() 362 && isValidDevice(device)) { 363 try { 364 return mService.getAudioConfig(device); 365 } catch (RemoteException e) { 366 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 367 return null; 368 } 369 } 370 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 371 return null; 372 } 373 374 /** 375 * Set priority of the profile 376 * 377 * <p> The device should already be paired. 378 * Priority can be one of {@link #PRIORITY_ON} orgetBluetoothManager 379 * {@link #PRIORITY_OFF}, 380 * 381 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 382 * permission. 383 * 384 * @param device Paired bluetooth device 385 * @param priority 386 * @return true if priority is set, false on error 387 * @hide 388 */ setPriority(BluetoothDevice device, int priority)389 public boolean setPriority(BluetoothDevice device, int priority) { 390 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 391 if (mService != null && isEnabled() 392 && isValidDevice(device)) { 393 if (priority != BluetoothProfile.PRIORITY_OFF && 394 priority != BluetoothProfile.PRIORITY_ON){ 395 return false; 396 } 397 try { 398 return mService.setPriority(device, priority); 399 } catch (RemoteException e) { 400 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 401 return false; 402 } 403 } 404 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 405 return false; 406 } 407 408 /** 409 * Get the priority of the profile. 410 * 411 * <p> The priority can be any of: 412 * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, 413 * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} 414 * 415 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 416 * 417 * @param device Bluetooth device 418 * @return priority of the device 419 * @hide 420 */ getPriority(BluetoothDevice device)421 public int getPriority(BluetoothDevice device) { 422 if (VDBG) log("getPriority(" + device + ")"); 423 if (mService != null && isEnabled() 424 && isValidDevice(device)) { 425 try { 426 return mService.getPriority(device); 427 } catch (RemoteException e) { 428 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 429 return BluetoothProfile.PRIORITY_OFF; 430 } 431 } 432 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 433 return BluetoothProfile.PRIORITY_OFF; 434 } 435 436 /** 437 * Check if A2DP profile is streaming music. 438 * 439 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 440 * 441 * @param device BluetoothDevice device 442 */ isA2dpPlaying(BluetoothDevice device)443 public boolean isA2dpPlaying(BluetoothDevice device) { 444 if (mService != null && isEnabled() 445 && isValidDevice(device)) { 446 try { 447 return mService.isA2dpPlaying(device); 448 } catch (RemoteException e) { 449 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 450 return false; 451 } 452 } 453 if (mService == null) Log.w(TAG, "Proxy not attached to service"); 454 return false; 455 } 456 457 /** 458 * Helper for converting a state to a string. 459 * 460 * For debug use only - strings are not internationalized. 461 * @hide 462 */ stateToString(int state)463 public static String stateToString(int state) { 464 switch (state) { 465 case STATE_DISCONNECTED: 466 return "disconnected"; 467 case STATE_CONNECTING: 468 return "connecting"; 469 case STATE_CONNECTED: 470 return "connected"; 471 case STATE_DISCONNECTING: 472 return "disconnecting"; 473 case STATE_PLAYING: 474 return "playing"; 475 case STATE_NOT_PLAYING: 476 return "not playing"; 477 default: 478 return "<unknown state " + state + ">"; 479 } 480 } 481 482 private final ServiceConnection mConnection = new ServiceConnection() { 483 public void onServiceConnected(ComponentName className, IBinder service) { 484 if (DBG) Log.d(TAG, "Proxy object connected"); 485 mService = IBluetoothA2dpSink.Stub.asInterface(Binder.allowBlocking(service)); 486 487 if (mServiceListener != null) { 488 mServiceListener.onServiceConnected(BluetoothProfile.A2DP_SINK, 489 BluetoothA2dpSink.this); 490 } 491 } 492 public void onServiceDisconnected(ComponentName className) { 493 if (DBG) Log.d(TAG, "Proxy object disconnected"); 494 mService = null; 495 if (mServiceListener != null) { 496 mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP_SINK); 497 } 498 } 499 }; 500 isEnabled()501 private boolean isEnabled() { 502 if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; 503 return false; 504 } 505 isValidDevice(BluetoothDevice device)506 private boolean isValidDevice(BluetoothDevice device) { 507 if (device == null) return false; 508 509 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 510 return false; 511 } 512 log(String msg)513 private static void log(String msg) { 514 Log.d(TAG, msg); 515 } 516 } 517