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.IBinder; 24 import android.os.RemoteException; 25 import android.util.Log; 26 27 import java.util.ArrayList; 28 import 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 */ 40 public 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 */ BluetoothA2dpSink(Context context, ServiceListener l)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 doBind()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 close()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 finalize()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 */ connect(BluetoothDevice device)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 */ disconnect(BluetoothDevice device)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 */ getConnectedDevices()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 */ getDevicesMatchingConnectionStates(int[] states)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 */ getConnectionState(BluetoothDevice device)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 */ getAudioConfig(BluetoothDevice device)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 * Helper for converting a state to a string. 375 * 376 * For debug use only - strings are not internationalized. 377 * @hide 378 */ stateToString(int state)379 public static String stateToString(int state) { 380 switch (state) { 381 case STATE_DISCONNECTED: 382 return "disconnected"; 383 case STATE_CONNECTING: 384 return "connecting"; 385 case STATE_CONNECTED: 386 return "connected"; 387 case STATE_DISCONNECTING: 388 return "disconnecting"; 389 case STATE_PLAYING: 390 return "playing"; 391 case STATE_NOT_PLAYING: 392 return "not playing"; 393 default: 394 return "<unknown state " + state + ">"; 395 } 396 } 397 398 private final ServiceConnection mConnection = new ServiceConnection() { 399 public void onServiceConnected(ComponentName className, IBinder service) { 400 if (DBG) Log.d(TAG, "Proxy object connected"); 401 mService = IBluetoothA2dpSink.Stub.asInterface(service); 402 403 if (mServiceListener != null) { 404 mServiceListener.onServiceConnected(BluetoothProfile.A2DP_SINK, 405 BluetoothA2dpSink.this); 406 } 407 } 408 public void onServiceDisconnected(ComponentName className) { 409 if (DBG) Log.d(TAG, "Proxy object disconnected"); 410 mService = null; 411 if (mServiceListener != null) { 412 mServiceListener.onServiceDisconnected(BluetoothProfile.A2DP_SINK); 413 } 414 } 415 }; 416 isEnabled()417 private boolean isEnabled() { 418 if (mAdapter.getState() == BluetoothAdapter.STATE_ON) return true; 419 return false; 420 } 421 isValidDevice(BluetoothDevice device)422 private boolean isValidDevice(BluetoothDevice device) { 423 if (device == null) return false; 424 425 if (BluetoothAdapter.checkBluetoothAddress(device.getAddress())) return true; 426 return false; 427 } 428 log(String msg)429 private static void log(String msg) { 430 Log.d(TAG, msg); 431 } 432 } 433