1 /* 2 * Copyright (C) 2008 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 /** 18 * TODO: Move this to services.jar 19 * and make the contructor package private again. 20 * @hide 21 */ 22 23 package android.server; 24 25 import android.bluetooth.BluetoothA2dp; 26 import android.bluetooth.BluetoothAdapter; 27 import android.bluetooth.BluetoothDevice; 28 import android.bluetooth.BluetoothUuid; 29 import android.bluetooth.IBluetoothA2dp; 30 import android.os.ParcelUuid; 31 import android.content.BroadcastReceiver; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.media.AudioManager; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.provider.Settings; 39 import android.util.Log; 40 41 import java.io.FileDescriptor; 42 import java.io.PrintWriter; 43 import java.util.HashMap; 44 import java.util.HashSet; 45 import java.util.Set; 46 47 public class BluetoothA2dpService extends IBluetoothA2dp.Stub { 48 private static final String TAG = "BluetoothA2dpService"; 49 private static final boolean DBG = true; 50 51 public static final String BLUETOOTH_A2DP_SERVICE = "bluetooth_a2dp"; 52 53 private static final String BLUETOOTH_ADMIN_PERM = android.Manifest.permission.BLUETOOTH_ADMIN; 54 private static final String BLUETOOTH_PERM = android.Manifest.permission.BLUETOOTH; 55 56 private static final String BLUETOOTH_ENABLED = "bluetooth_enabled"; 57 58 private static final int MESSAGE_CONNECT_TO = 1; 59 60 private static final String PROPERTY_STATE = "State"; 61 62 private static final String SINK_STATE_DISCONNECTED = "disconnected"; 63 private static final String SINK_STATE_CONNECTING = "connecting"; 64 private static final String SINK_STATE_CONNECTED = "connected"; 65 private static final String SINK_STATE_PLAYING = "playing"; 66 67 private static int mSinkCount; 68 69 private final Context mContext; 70 private final IntentFilter mIntentFilter; 71 private HashMap<BluetoothDevice, Integer> mAudioDevices; 72 private final AudioManager mAudioManager; 73 private final BluetoothService mBluetoothService; 74 private final BluetoothAdapter mAdapter; 75 private int mTargetA2dpState; 76 77 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 78 @Override 79 public void onReceive(Context context, Intent intent) { 80 String action = intent.getAction(); 81 BluetoothDevice device = 82 intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 83 if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { 84 int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 85 BluetoothAdapter.ERROR); 86 switch (state) { 87 case BluetoothAdapter.STATE_ON: 88 onBluetoothEnable(); 89 break; 90 case BluetoothAdapter.STATE_TURNING_OFF: 91 onBluetoothDisable(); 92 break; 93 } 94 } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { 95 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 96 BluetoothDevice.ERROR); 97 switch(bondState) { 98 case BluetoothDevice.BOND_BONDED: 99 setSinkPriority(device, BluetoothA2dp.PRIORITY_AUTO); 100 break; 101 case BluetoothDevice.BOND_BONDING: 102 case BluetoothDevice.BOND_NONE: 103 setSinkPriority(device, BluetoothA2dp.PRIORITY_OFF); 104 break; 105 } 106 } else if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) { 107 if (getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && 108 isSinkDevice(device)) { 109 // This device is a preferred sink. Make an A2DP connection 110 // after a delay. We delay to avoid connection collisions, 111 // and to give other profiles such as HFP a chance to 112 // connect first. 113 Message msg = Message.obtain(mHandler, MESSAGE_CONNECT_TO, device); 114 mHandler.sendMessageDelayed(msg, 6000); 115 } 116 } else if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) { 117 synchronized (this) { 118 if (mAudioDevices.containsKey(device)) { 119 int state = mAudioDevices.get(device); 120 handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); 121 } 122 } 123 } 124 } 125 }; 126 BluetoothA2dpService(Context context, BluetoothService bluetoothService)127 public BluetoothA2dpService(Context context, BluetoothService bluetoothService) { 128 mContext = context; 129 130 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 131 132 mBluetoothService = bluetoothService; 133 if (mBluetoothService == null) { 134 throw new RuntimeException("Platform does not support Bluetooth"); 135 } 136 137 if (!initNative()) { 138 throw new RuntimeException("Could not init BluetoothA2dpService"); 139 } 140 141 mAdapter = BluetoothAdapter.getDefaultAdapter(); 142 143 mIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); 144 mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 145 mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); 146 mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); 147 mContext.registerReceiver(mReceiver, mIntentFilter); 148 149 mAudioDevices = new HashMap<BluetoothDevice, Integer>(); 150 151 if (mBluetoothService.isEnabled()) 152 onBluetoothEnable(); 153 mTargetA2dpState = -1; 154 } 155 156 @Override finalize()157 protected void finalize() throws Throwable { 158 try { 159 cleanupNative(); 160 } finally { 161 super.finalize(); 162 } 163 } 164 165 private final Handler mHandler = new Handler() { 166 @Override 167 public void handleMessage(Message msg) { 168 switch (msg.what) { 169 case MESSAGE_CONNECT_TO: 170 BluetoothDevice device = (BluetoothDevice) msg.obj; 171 // check bluetooth is still on, device is still preferred, and 172 // nothing is currently connected 173 if (mBluetoothService.isEnabled() && 174 getSinkPriority(device) > BluetoothA2dp.PRIORITY_OFF && 175 lookupSinksMatchingStates(new int[] { 176 BluetoothA2dp.STATE_CONNECTING, 177 BluetoothA2dp.STATE_CONNECTED, 178 BluetoothA2dp.STATE_PLAYING, 179 BluetoothA2dp.STATE_DISCONNECTING}).size() == 0) { 180 log("Auto-connecting A2DP to sink " + device); 181 connectSink(device); 182 } 183 break; 184 } 185 } 186 }; 187 convertBluezSinkStringtoState(String value)188 private int convertBluezSinkStringtoState(String value) { 189 if (value.equalsIgnoreCase("disconnected")) 190 return BluetoothA2dp.STATE_DISCONNECTED; 191 if (value.equalsIgnoreCase("connecting")) 192 return BluetoothA2dp.STATE_CONNECTING; 193 if (value.equalsIgnoreCase("connected")) 194 return BluetoothA2dp.STATE_CONNECTED; 195 if (value.equalsIgnoreCase("playing")) 196 return BluetoothA2dp.STATE_PLAYING; 197 return -1; 198 } 199 isSinkDevice(BluetoothDevice device)200 private boolean isSinkDevice(BluetoothDevice device) { 201 ParcelUuid[] uuids = mBluetoothService.getRemoteUuids(device.getAddress()); 202 if (uuids != null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) { 203 return true; 204 } 205 return false; 206 } 207 addAudioSink(BluetoothDevice device)208 private synchronized boolean addAudioSink (BluetoothDevice device) { 209 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 210 String propValues[] = (String []) getSinkPropertiesNative(path); 211 if (propValues == null) { 212 Log.e(TAG, "Error while getting AudioSink properties for device: " + device); 213 return false; 214 } 215 Integer state = null; 216 // Properties are name-value pairs 217 for (int i = 0; i < propValues.length; i+=2) { 218 if (propValues[i].equals(PROPERTY_STATE)) { 219 state = new Integer(convertBluezSinkStringtoState(propValues[i+1])); 220 break; 221 } 222 } 223 mAudioDevices.put(device, state); 224 handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTED, state); 225 return true; 226 } 227 onBluetoothEnable()228 private synchronized void onBluetoothEnable() { 229 String devices = mBluetoothService.getProperty("Devices"); 230 mSinkCount = 0; 231 if (devices != null) { 232 String [] paths = devices.split(","); 233 for (String path: paths) { 234 String address = mBluetoothService.getAddressFromObjectPath(path); 235 BluetoothDevice device = mAdapter.getRemoteDevice(address); 236 ParcelUuid[] remoteUuids = mBluetoothService.getRemoteUuids(address); 237 if (remoteUuids != null) 238 if (BluetoothUuid.containsAnyUuid(remoteUuids, 239 new ParcelUuid[] {BluetoothUuid.AudioSink, 240 BluetoothUuid.AdvAudioDist})) { 241 addAudioSink(device); 242 } 243 } 244 } 245 mAudioManager.setParameters(BLUETOOTH_ENABLED+"=true"); 246 mAudioManager.setParameters("A2dpSuspended=false"); 247 } 248 onBluetoothDisable()249 private synchronized void onBluetoothDisable() { 250 if (!mAudioDevices.isEmpty()) { 251 BluetoothDevice[] devices = new BluetoothDevice[mAudioDevices.size()]; 252 devices = mAudioDevices.keySet().toArray(devices); 253 for (BluetoothDevice device : devices) { 254 int state = getSinkState(device); 255 switch (state) { 256 case BluetoothA2dp.STATE_CONNECTING: 257 case BluetoothA2dp.STATE_CONNECTED: 258 case BluetoothA2dp.STATE_PLAYING: 259 disconnectSinkNative(mBluetoothService.getObjectPathFromAddress( 260 device.getAddress())); 261 handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); 262 break; 263 case BluetoothA2dp.STATE_DISCONNECTING: 264 handleSinkStateChange(device, BluetoothA2dp.STATE_DISCONNECTING, 265 BluetoothA2dp.STATE_DISCONNECTED); 266 break; 267 } 268 } 269 mAudioDevices.clear(); 270 } 271 272 mAudioManager.setParameters(BLUETOOTH_ENABLED + "=false"); 273 } 274 connectSink(BluetoothDevice device)275 public synchronized boolean connectSink(BluetoothDevice device) { 276 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 277 "Need BLUETOOTH_ADMIN permission"); 278 if (DBG) log("connectSink(" + device + ")"); 279 280 // ignore if there are any active sinks 281 if (lookupSinksMatchingStates(new int[] { 282 BluetoothA2dp.STATE_CONNECTING, 283 BluetoothA2dp.STATE_CONNECTED, 284 BluetoothA2dp.STATE_PLAYING, 285 BluetoothA2dp.STATE_DISCONNECTING}).size() != 0) { 286 return false; 287 } 288 289 if (mAudioDevices.get(device) == null && !addAudioSink(device)) 290 return false; 291 292 int state = mAudioDevices.get(device); 293 294 switch (state) { 295 case BluetoothA2dp.STATE_CONNECTED: 296 case BluetoothA2dp.STATE_PLAYING: 297 case BluetoothA2dp.STATE_DISCONNECTING: 298 return false; 299 case BluetoothA2dp.STATE_CONNECTING: 300 return true; 301 } 302 303 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 304 if (path == null) 305 return false; 306 307 // State is DISCONNECTED 308 if (!connectSinkNative(path)) { 309 return false; 310 } 311 return true; 312 } 313 disconnectSink(BluetoothDevice device)314 public synchronized boolean disconnectSink(BluetoothDevice device) { 315 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 316 "Need BLUETOOTH_ADMIN permission"); 317 if (DBG) log("disconnectSink(" + device + ")"); 318 319 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 320 if (path == null) { 321 return false; 322 } 323 324 switch (getSinkState(device)) { 325 case BluetoothA2dp.STATE_DISCONNECTED: 326 return false; 327 case BluetoothA2dp.STATE_DISCONNECTING: 328 return true; 329 } 330 331 // State is CONNECTING or CONNECTED or PLAYING 332 if (!disconnectSinkNative(path)) { 333 return false; 334 } else { 335 return true; 336 } 337 } 338 suspendSink(BluetoothDevice device)339 public synchronized boolean suspendSink(BluetoothDevice device) { 340 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 341 "Need BLUETOOTH_ADMIN permission"); 342 if (DBG) log("suspendSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); 343 if (device == null || mAudioDevices == null) { 344 return false; 345 } 346 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 347 Integer state = mAudioDevices.get(device); 348 if (path == null || state == null) { 349 return false; 350 } 351 352 mTargetA2dpState = BluetoothA2dp.STATE_CONNECTED; 353 return checkSinkSuspendState(state.intValue()); 354 } 355 resumeSink(BluetoothDevice device)356 public synchronized boolean resumeSink(BluetoothDevice device) { 357 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 358 "Need BLUETOOTH_ADMIN permission"); 359 if (DBG) log("resumeSink(" + device + "), mTargetA2dpState: "+mTargetA2dpState); 360 if (device == null || mAudioDevices == null) { 361 return false; 362 } 363 String path = mBluetoothService.getObjectPathFromAddress(device.getAddress()); 364 Integer state = mAudioDevices.get(device); 365 if (path == null || state == null) { 366 return false; 367 } 368 mTargetA2dpState = BluetoothA2dp.STATE_PLAYING; 369 return checkSinkSuspendState(state.intValue()); 370 } 371 getConnectedSinks()372 public synchronized BluetoothDevice[] getConnectedSinks() { 373 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 374 Set<BluetoothDevice> sinks = lookupSinksMatchingStates( 375 new int[] {BluetoothA2dp.STATE_CONNECTED, BluetoothA2dp.STATE_PLAYING}); 376 return sinks.toArray(new BluetoothDevice[sinks.size()]); 377 } 378 getSinkState(BluetoothDevice device)379 public synchronized int getSinkState(BluetoothDevice device) { 380 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 381 Integer state = mAudioDevices.get(device); 382 if (state == null) 383 return BluetoothA2dp.STATE_DISCONNECTED; 384 return state; 385 } 386 getSinkPriority(BluetoothDevice device)387 public synchronized int getSinkPriority(BluetoothDevice device) { 388 mContext.enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); 389 return Settings.Secure.getInt(mContext.getContentResolver(), 390 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), 391 BluetoothA2dp.PRIORITY_OFF); 392 } 393 setSinkPriority(BluetoothDevice device, int priority)394 public synchronized boolean setSinkPriority(BluetoothDevice device, int priority) { 395 mContext.enforceCallingOrSelfPermission(BLUETOOTH_ADMIN_PERM, 396 "Need BLUETOOTH_ADMIN permission"); 397 if (!BluetoothAdapter.checkBluetoothAddress(device.getAddress())) { 398 return false; 399 } 400 return Settings.Secure.putInt(mContext.getContentResolver(), 401 Settings.Secure.getBluetoothA2dpSinkPriorityKey(device.getAddress()), priority); 402 } 403 onSinkPropertyChanged(String path, String []propValues)404 private synchronized void onSinkPropertyChanged(String path, String []propValues) { 405 if (!mBluetoothService.isEnabled()) { 406 return; 407 } 408 409 String name = propValues[0]; 410 String address = mBluetoothService.getAddressFromObjectPath(path); 411 if (address == null) { 412 Log.e(TAG, "onSinkPropertyChanged: Address of the remote device in null"); 413 return; 414 } 415 416 BluetoothDevice device = mAdapter.getRemoteDevice(address); 417 418 if (name.equals(PROPERTY_STATE)) { 419 int state = convertBluezSinkStringtoState(propValues[1]); 420 if (mAudioDevices.get(device) == null) { 421 // This is for an incoming connection for a device not known to us. 422 // We have authorized it and bluez state has changed. 423 addAudioSink(device); 424 } else { 425 int prevState = mAudioDevices.get(device); 426 handleSinkStateChange(device, prevState, state); 427 } 428 } 429 } 430 handleSinkStateChange(BluetoothDevice device, int prevState, int state)431 private void handleSinkStateChange(BluetoothDevice device, int prevState, int state) { 432 if (state != prevState) { 433 if (state == BluetoothA2dp.STATE_DISCONNECTED || 434 state == BluetoothA2dp.STATE_DISCONNECTING) { 435 if (prevState == BluetoothA2dp.STATE_CONNECTED || 436 prevState == BluetoothA2dp.STATE_PLAYING) { 437 // disconnecting or disconnected 438 Intent intent = new Intent(AudioManager.ACTION_AUDIO_BECOMING_NOISY); 439 mContext.sendBroadcast(intent); 440 } 441 mSinkCount--; 442 } else if (state == BluetoothA2dp.STATE_CONNECTED) { 443 mSinkCount ++; 444 } 445 mAudioDevices.put(device, state); 446 447 checkSinkSuspendState(state); 448 mTargetA2dpState = -1; 449 450 if (state == BluetoothA2dp.STATE_CONNECTING) { 451 mAudioManager.setParameters("A2dpSuspended=false"); 452 } 453 Intent intent = new Intent(BluetoothA2dp.ACTION_SINK_STATE_CHANGED); 454 intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device); 455 intent.putExtra(BluetoothA2dp.EXTRA_PREVIOUS_SINK_STATE, prevState); 456 intent.putExtra(BluetoothA2dp.EXTRA_SINK_STATE, state); 457 mContext.sendBroadcast(intent, BLUETOOTH_PERM); 458 459 if (DBG) log("A2DP state : device: " + device + " State:" + prevState + "->" + state); 460 } 461 } 462 lookupSinksMatchingStates(int[] states)463 private synchronized Set<BluetoothDevice> lookupSinksMatchingStates(int[] states) { 464 Set<BluetoothDevice> sinks = new HashSet<BluetoothDevice>(); 465 if (mAudioDevices.isEmpty()) { 466 return sinks; 467 } 468 for (BluetoothDevice device: mAudioDevices.keySet()) { 469 int sinkState = getSinkState(device); 470 for (int state : states) { 471 if (state == sinkState) { 472 sinks.add(device); 473 break; 474 } 475 } 476 } 477 return sinks; 478 } 479 checkSinkSuspendState(int state)480 private boolean checkSinkSuspendState(int state) { 481 boolean result = true; 482 483 if (state != mTargetA2dpState) { 484 if (state == BluetoothA2dp.STATE_PLAYING && 485 mTargetA2dpState == BluetoothA2dp.STATE_CONNECTED) { 486 mAudioManager.setParameters("A2dpSuspended=true"); 487 } else if (state == BluetoothA2dp.STATE_CONNECTED && 488 mTargetA2dpState == BluetoothA2dp.STATE_PLAYING) { 489 mAudioManager.setParameters("A2dpSuspended=false"); 490 } else { 491 result = false; 492 } 493 } 494 return result; 495 } 496 497 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)498 protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 499 if (mAudioDevices.isEmpty()) return; 500 pw.println("Cached audio devices:"); 501 for (BluetoothDevice device : mAudioDevices.keySet()) { 502 int state = mAudioDevices.get(device); 503 pw.println(device + " " + BluetoothA2dp.stateToString(state)); 504 } 505 } 506 log(String msg)507 private static void log(String msg) { 508 Log.d(TAG, msg); 509 } 510 initNative()511 private native boolean initNative(); cleanupNative()512 private native void cleanupNative(); connectSinkNative(String path)513 private synchronized native boolean connectSinkNative(String path); disconnectSinkNative(String path)514 private synchronized native boolean disconnectSinkNative(String path); suspendSinkNative(String path)515 private synchronized native boolean suspendSinkNative(String path); resumeSinkNative(String path)516 private synchronized native boolean resumeSinkNative(String path); getSinkPropertiesNative(String path)517 private synchronized native Object []getSinkPropertiesNative(String path); 518 } 519