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 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 final IBluetoothA2dpSink service = mService; 243 if (service != null && isEnabled() && isValidDevice(device)) { 244 try { 245 return service.connect(device); 246 } catch (RemoteException e) { 247 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 248 return false; 249 } 250 } 251 if (service == 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 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 */ getConnectedDevices()299 public List<BluetoothDevice> getConnectedDevices() { 300 if (VDBG) log("getConnectedDevices()"); 301 final IBluetoothA2dpSink service = mService; 302 if (service != null && isEnabled()) { 303 try { 304 return service.getConnectedDevices(); 305 } catch (RemoteException e) { 306 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 307 return new ArrayList<BluetoothDevice>(); 308 } 309 } 310 if (service == null) Log.w(TAG, "Proxy not attached to service"); 311 return new ArrayList<BluetoothDevice>(); 312 } 313 314 /** 315 * {@inheritDoc} 316 */ getDevicesMatchingConnectionStates(int[] states)317 public List<BluetoothDevice> getDevicesMatchingConnectionStates(int[] states) { 318 if (VDBG) log("getDevicesMatchingStates()"); 319 final IBluetoothA2dpSink service = mService; 320 if (service != null && isEnabled()) { 321 try { 322 return service.getDevicesMatchingConnectionStates(states); 323 } catch (RemoteException e) { 324 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 325 return new ArrayList<BluetoothDevice>(); 326 } 327 } 328 if (service == null) Log.w(TAG, "Proxy not attached to service"); 329 return new ArrayList<BluetoothDevice>(); 330 } 331 332 /** 333 * {@inheritDoc} 334 */ getConnectionState(BluetoothDevice device)335 public int getConnectionState(BluetoothDevice device) { 336 if (VDBG) log("getState(" + device + ")"); 337 final IBluetoothA2dpSink service = mService; 338 if (service != null && isEnabled() && isValidDevice(device)) { 339 try { 340 return service.getConnectionState(device); 341 } catch (RemoteException e) { 342 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 343 return BluetoothProfile.STATE_DISCONNECTED; 344 } 345 } 346 if (service == null) Log.w(TAG, "Proxy not attached to service"); 347 return BluetoothProfile.STATE_DISCONNECTED; 348 } 349 350 /** 351 * Get the current audio configuration for the A2DP source device, 352 * or null if the device has no audio configuration 353 * 354 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 355 * 356 * @param device Remote bluetooth device. 357 * @return audio configuration for the device, or null 358 * 359 * {@see BluetoothAudioConfig} 360 */ getAudioConfig(BluetoothDevice device)361 public BluetoothAudioConfig getAudioConfig(BluetoothDevice device) { 362 if (VDBG) log("getAudioConfig(" + device + ")"); 363 final IBluetoothA2dpSink service = mService; 364 if (service != null && isEnabled() && isValidDevice(device)) { 365 try { 366 return service.getAudioConfig(device); 367 } catch (RemoteException e) { 368 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 369 return null; 370 } 371 } 372 if (service == null) Log.w(TAG, "Proxy not attached to service"); 373 return null; 374 } 375 376 /** 377 * Set priority of the profile 378 * 379 * <p> The device should already be paired. 380 * Priority can be one of {@link #PRIORITY_ON} orgetBluetoothManager 381 * {@link #PRIORITY_OFF}, 382 * 383 * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} 384 * permission. 385 * 386 * @param device Paired bluetooth device 387 * @param priority 388 * @return true if priority is set, false on error 389 * @hide 390 */ setPriority(BluetoothDevice device, int priority)391 public boolean setPriority(BluetoothDevice device, int priority) { 392 if (DBG) log("setPriority(" + device + ", " + priority + ")"); 393 final IBluetoothA2dpSink service = mService; 394 if (service != null && isEnabled() && isValidDevice(device)) { 395 if (priority != BluetoothProfile.PRIORITY_OFF 396 && priority != BluetoothProfile.PRIORITY_ON) { 397 return false; 398 } 399 try { 400 return service.setPriority(device, priority); 401 } catch (RemoteException e) { 402 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 403 return false; 404 } 405 } 406 if (service == null) Log.w(TAG, "Proxy not attached to service"); 407 return false; 408 } 409 410 /** 411 * Get the priority of the profile. 412 * 413 * <p> The priority can be any of: 414 * {@link #PRIORITY_AUTO_CONNECT}, {@link #PRIORITY_OFF}, 415 * {@link #PRIORITY_ON}, {@link #PRIORITY_UNDEFINED} 416 * 417 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 418 * 419 * @param device Bluetooth device 420 * @return priority of the device 421 * @hide 422 */ getPriority(BluetoothDevice device)423 public int getPriority(BluetoothDevice device) { 424 if (VDBG) log("getPriority(" + device + ")"); 425 final IBluetoothA2dpSink service = mService; 426 if (service != null && isEnabled() && isValidDevice(device)) { 427 try { 428 return service.getPriority(device); 429 } catch (RemoteException e) { 430 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 431 return BluetoothProfile.PRIORITY_OFF; 432 } 433 } 434 if (service == null) Log.w(TAG, "Proxy not attached to service"); 435 return BluetoothProfile.PRIORITY_OFF; 436 } 437 438 /** 439 * Check if A2DP profile is streaming music. 440 * 441 * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission. 442 * 443 * @param device BluetoothDevice device 444 */ isA2dpPlaying(BluetoothDevice device)445 public boolean isA2dpPlaying(BluetoothDevice device) { 446 final IBluetoothA2dpSink service = mService; 447 if (service != null && isEnabled() && isValidDevice(device)) { 448 try { 449 return service.isA2dpPlaying(device); 450 } catch (RemoteException e) { 451 Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable())); 452 return false; 453 } 454 } 455 if (service == null) Log.w(TAG, "Proxy not attached to service"); 456 return false; 457 } 458 459 /** 460 * Helper for converting a state to a string. 461 * 462 * For debug use only - strings are not internationalized. 463 * @hide 464 */ stateToString(int state)465 public static String stateToString(int state) { 466 switch (state) { 467 case STATE_DISCONNECTED: 468 return "disconnected"; 469 case STATE_CONNECTING: 470 return "connecting"; 471 case STATE_CONNECTED: 472 return "connected"; 473 case STATE_DISCONNECTING: 474 return "disconnecting"; 475 case STATE_PLAYING: 476 return "playing"; 477 case STATE_NOT_PLAYING: 478 return "not playing"; 479 default: 480 return "<unknown state " + state + ">"; 481 } 482 } 483 484 private final ServiceConnection mConnection = new ServiceConnection() { 485 public void onServiceConnected(ComponentName className, IBinder service) { 486 if (DBG) Log.d(TAG, "Proxy object connected"); 487 mService = IBluetoothA2dpSink.Stub.asInterface(Binder.allowBlocking(service)); 488 if (mServiceListener != null) { 489 mServiceListener.onServiceConnected(BluetoothProfile.A2DP_SINK, 490 BluetoothA2dpSink.this); 491 } 492 } 493 public void onServiceDisconnected(ComponentName className) { 494 if (DBG) Log.d(TAG, "Proxy object disconnected"); 495 mService = null; 496 if (mServiceListener != null) { 497 mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP_SINK); 498 } 499 } 500 }; 501 isEnabled()502 private boolean isEnabled() { 503 return mAdapter.getState() == BluetoothAdapter.STATE_ON; 504 } 505 isValidDevice(BluetoothDevice device)506 private static boolean isValidDevice(BluetoothDevice device) { 507 return device != null && BluetoothAdapter.checkBluetoothAddress(device.getAddress()); 508 } 509 log(String msg)510 private static void log(String msg) { 511 Log.d(TAG, msg); 512 } 513 } 514