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