1 /* 2 * Copyright (C) 2017 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 com.googlecode.android_scripting.facade.bluetooth; 18 19 import android.app.Service; 20 import android.bluetooth.BluetoothA2dp; 21 import android.bluetooth.BluetoothA2dpSink; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothAdapter.OobDataCallback; 24 import android.bluetooth.BluetoothDevice; 25 import android.bluetooth.BluetoothHeadset; 26 import android.bluetooth.BluetoothHeadsetClient; 27 import android.bluetooth.BluetoothHidDevice; 28 import android.bluetooth.BluetoothHidHost; 29 import android.bluetooth.BluetoothManager; 30 import android.bluetooth.BluetoothMap; 31 import android.bluetooth.BluetoothMapClient; 32 import android.bluetooth.BluetoothPan; 33 import android.bluetooth.BluetoothPbapClient; 34 import android.bluetooth.BluetoothProfile; 35 import android.bluetooth.BluetoothUuid; 36 import android.bluetooth.OobData; 37 import android.content.BroadcastReceiver; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.IntentFilter; 41 import android.os.Bundle; 42 import android.os.ParcelUuid; 43 44 import com.googlecode.android_scripting.Log; 45 import com.googlecode.android_scripting.facade.EventFacade; 46 import com.googlecode.android_scripting.facade.FacadeManager; 47 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 48 import com.googlecode.android_scripting.rpc.Rpc; 49 import com.googlecode.android_scripting.rpc.RpcDefault; 50 import com.googlecode.android_scripting.rpc.RpcOptional; 51 import com.googlecode.android_scripting.rpc.RpcParameter; 52 53 import org.json.JSONArray; 54 import org.json.JSONException; 55 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collections; 59 import java.util.HashMap; 60 import java.util.List; 61 import java.util.Map; 62 63 public class BluetoothConnectionFacade extends RpcReceiver { 64 65 private final Service mService; 66 private final Context mContext; 67 private final BluetoothAdapter mBluetoothAdapter; 68 private final BluetoothManager mBluetoothManager; 69 private final BluetoothPairingHelper mPairingHelper; 70 private final Map<String, BroadcastReceiver> listeningDevices; 71 private final EventFacade mEventFacade; 72 private final OobDataCallback mGenerateOobDataCallback = new OobDataCallback() { 73 @Override 74 public void onError(int error) { 75 Log.d("onError: " + error); 76 Bundle results = new Bundle(); 77 results.putInt("Error", error); 78 mEventFacade.postEvent("ErrorOobData", results.clone()); 79 } 80 81 @Override 82 public void onOobData(int transport, OobData data) { 83 Log.d("Transport: " + transport); 84 Log.d("OobData: " + data); 85 Bundle results = new Bundle(); 86 results.putInt("transport", transport); 87 // Just what we need create a bond 88 results.putString("address_with_type", 89 toHexString(data.getDeviceAddressWithType())); 90 results.putString("confirmation", toHexString(data.getConfirmationHash())); 91 results.putString("randomizer", toHexString(data.getRandomizerHash())); 92 mEventFacade.postEvent("GeneratedOobData", results.clone()); 93 } 94 }; 95 96 private final IntentFilter mDiscoverConnectFilter; 97 private final IntentFilter mPairingFilter; 98 private final IntentFilter mBondFilter; 99 private final IntentFilter mA2dpStateChangeFilter; 100 private final IntentFilter mA2dpSinkStateChangeFilter; 101 private final IntentFilter mHidStateChangeFilter; 102 private final IntentFilter mHidDeviceStateChangeFilter; 103 private final IntentFilter mHspStateChangeFilter; 104 private final IntentFilter mHfpClientStateChangeFilter; 105 private final IntentFilter mPbapClientStateChangeFilter; 106 private final IntentFilter mPanStateChangeFilter; 107 private final IntentFilter mMapClientStateChangeFilter; 108 private final IntentFilter mMapStateChangeFilter; 109 110 private final Bundle mGoodNews; 111 private final Bundle mBadNews; 112 113 private BluetoothA2dpFacade mA2dpProfile; 114 private BluetoothA2dpSinkFacade mA2dpSinkProfile; 115 private BluetoothHidFacade mHidProfile; 116 private BluetoothHidDeviceFacade mHidDeviceProfile; 117 private BluetoothHspFacade mHspProfile; 118 private BluetoothHfpClientFacade mHfpClientProfile; 119 private BluetoothPbapClientFacade mPbapClientProfile; 120 private BluetoothPanFacade mPanProfile; 121 private BluetoothMapClientFacade mMapClientProfile; 122 private BluetoothMapFacade mMapProfile; 123 private ArrayList<String> mDeviceMonitorList; 124 BluetoothConnectionFacade(FacadeManager manager)125 public BluetoothConnectionFacade(FacadeManager manager) { 126 super(manager); 127 mService = manager.getService(); 128 mContext = mService.getApplicationContext(); 129 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 130 mBluetoothManager = (BluetoothManager) mContext.getSystemService( 131 Service.BLUETOOTH_SERVICE); 132 mDeviceMonitorList = new ArrayList<String>(); 133 // Use a synchronized map to avoid racing problems 134 listeningDevices = Collections.synchronizedMap(new HashMap<String, BroadcastReceiver>()); 135 136 mEventFacade = manager.getReceiver(EventFacade.class); 137 mPairingHelper = new BluetoothPairingHelper(mEventFacade); 138 mA2dpProfile = manager.getReceiver(BluetoothA2dpFacade.class); 139 mA2dpSinkProfile = manager.getReceiver(BluetoothA2dpSinkFacade.class); 140 mHidProfile = manager.getReceiver(BluetoothHidFacade.class); 141 mHidDeviceProfile = manager.getReceiver(BluetoothHidDeviceFacade.class); 142 mHspProfile = manager.getReceiver(BluetoothHspFacade.class); 143 mHfpClientProfile = manager.getReceiver(BluetoothHfpClientFacade.class); 144 mPbapClientProfile = manager.getReceiver(BluetoothPbapClientFacade.class); 145 mPanProfile = manager.getReceiver(BluetoothPanFacade.class); 146 mMapClientProfile = manager.getReceiver(BluetoothMapClientFacade.class); 147 mMapProfile = manager.getReceiver(BluetoothMapFacade.class); 148 149 mDiscoverConnectFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND); 150 mDiscoverConnectFilter.addAction(BluetoothDevice.ACTION_UUID); 151 mDiscoverConnectFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 152 153 mPairingFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 154 mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); 155 mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REPLY); 156 mPairingFilter.setPriority(999); 157 158 mBondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 159 mBondFilter.addAction(BluetoothDevice.ACTION_FOUND); 160 mBondFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 161 162 mA2dpStateChangeFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 163 mA2dpSinkStateChangeFilter = 164 new IntentFilter(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); 165 mHidStateChangeFilter = 166 new IntentFilter(BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); 167 mHidDeviceStateChangeFilter = 168 new IntentFilter(BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED); 169 mHspStateChangeFilter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 170 mHfpClientStateChangeFilter = 171 new IntentFilter(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); 172 mPbapClientStateChangeFilter = 173 new IntentFilter(BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); 174 mPanStateChangeFilter = 175 new IntentFilter(BluetoothPan.ACTION_CONNECTION_STATE_CHANGED); 176 mMapClientStateChangeFilter = 177 new IntentFilter(BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); 178 mMapStateChangeFilter = 179 new IntentFilter(BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); 180 181 mGoodNews = new Bundle(); 182 mGoodNews.putBoolean("Status", true); 183 mBadNews = new Bundle(); 184 mBadNews.putBoolean("Status", false); 185 } 186 unregisterCachedListener(String listenerId)187 private void unregisterCachedListener(String listenerId) { 188 BroadcastReceiver listener = listeningDevices.remove(listenerId); 189 if (listener != null) { 190 mService.unregisterReceiver(listener); 191 } 192 } 193 194 /** 195 * Connect to a specific device upon its discovery 196 */ 197 public class DiscoverConnectReceiver extends BroadcastReceiver { 198 private final String mDeviceID; 199 private BluetoothDevice mDevice; 200 201 /** 202 * Constructor 203 * 204 * @param deviceID Either the device alias name or mac address. 205 * @param bond If true, bond the device only. 206 */ DiscoverConnectReceiver(String deviceID)207 public DiscoverConnectReceiver(String deviceID) { 208 super(); 209 mDeviceID = deviceID; 210 } 211 212 @Override onReceive(Context context, Intent intent)213 public void onReceive(Context context, Intent intent) { 214 String action = intent.getAction(); 215 // The specified device is found. 216 if (action.equals(BluetoothDevice.ACTION_FOUND)) { 217 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 218 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 219 Log.d("Found device " + device.getAlias() + " for connection."); 220 mBluetoothAdapter.cancelDiscovery(); 221 mDevice = device; 222 } 223 // After discovery stops. 224 } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { 225 if (mDevice == null) { 226 Log.d("Device " + mDeviceID + " not discovered."); 227 mEventFacade.postEvent("Bond" + mDeviceID, mBadNews); 228 return; 229 } 230 boolean status = mDevice.fetchUuidsWithSdp(); 231 Log.d("Initiated ACL connection: " + status); 232 } else if (action.equals(BluetoothDevice.ACTION_UUID)) { 233 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 234 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 235 Log.d("Initiating connections."); 236 connectProfile(device, mDeviceID); 237 mService.unregisterReceiver(listeningDevices.remove("Connect" + mDeviceID)); 238 } 239 } 240 } 241 } 242 243 /** 244 * Connect to a specific device upon its discovery 245 */ 246 public class DiscoverBondReceiver extends BroadcastReceiver { 247 private final String mDeviceID; 248 private BluetoothDevice mDevice = null; 249 private boolean started = false; 250 251 /** 252 * Constructor 253 * 254 * @param deviceID Either the device alias name or Mac address. 255 */ DiscoverBondReceiver(String deviceID)256 public DiscoverBondReceiver(String deviceID) { 257 super(); 258 mDeviceID = deviceID; 259 } 260 261 @Override onReceive(Context context, Intent intent)262 public void onReceive(Context context, Intent intent) { 263 String action = intent.getAction(); 264 // The specified device is found. 265 if (action.equals(BluetoothDevice.ACTION_FOUND)) { 266 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 267 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 268 Log.d("Found device " + device.getAlias() + " for connection."); 269 mEventFacade.postEvent("Discovery" + mDeviceID, mGoodNews); 270 mBluetoothAdapter.cancelDiscovery(); 271 mDevice = device; 272 } 273 // After discovery stops. 274 } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { 275 if (mDevice == null) { 276 Log.d("Device " + mDeviceID + " was not discovered."); 277 mEventFacade.postEvent("Discovery", mBadNews); 278 mEventFacade.postEvent("Bond", mBadNews); 279 return; 280 } 281 // Attempt to initiate bonding. 282 if (!started) { 283 Log.d("Bond with " + mDevice.getAlias()); 284 if (mDevice.createBond()) { 285 started = true; 286 Log.d("Bonding started."); 287 } else { 288 Log.e("Failed to bond with " + mDevice.getAlias()); 289 mEventFacade.postEvent("Bond", mBadNews); 290 mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID)); 291 } 292 } 293 } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { 294 Log.d("Bond state changing."); 295 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 296 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 297 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); 298 Log.d("New state is " + state); 299 if (state == BluetoothDevice.BOND_BONDED) { 300 Log.d("Bonding with " + mDeviceID + " successful."); 301 mEventFacade.postEvent("Bond" + mDeviceID, mGoodNews); 302 mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID)); 303 } 304 } 305 } 306 } 307 } 308 309 public class ConnectStateChangeReceiver extends BroadcastReceiver { 310 private final String mDeviceID; 311 ConnectStateChangeReceiver(String deviceID)312 public ConnectStateChangeReceiver(String deviceID) { 313 mDeviceID = deviceID; 314 } 315 316 @Override onReceive(Context context, Intent intent)317 public void onReceive(Context context, Intent intent) { 318 // no matter what the action, just push it... 319 String action = intent.getAction(); 320 Log.d("Action received: " + action); 321 322 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 323 // Check if received the specified device 324 if (!BluetoothFacade.deviceMatch(device, mDeviceID)) { 325 Log.e("Action devices does match act: " + device + " exp " + mDeviceID); 326 return; 327 } 328 // Find the state. 329 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 330 if (state == -1) { 331 Log.e("Action does not have a state."); 332 return; 333 } 334 335 // Switch Only Necessary for Old implementation. Left in for backwards compatability. 336 int profile = -1; 337 switch (action) { 338 case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED: 339 profile = BluetoothProfile.A2DP; 340 break; 341 case BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED: 342 profile = BluetoothProfile.HID_HOST; 343 break; 344 case BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED: 345 profile = BluetoothProfile.HEADSET; 346 break; 347 case BluetoothPan.ACTION_CONNECTION_STATE_CHANGED: 348 profile = BluetoothProfile.PAN; 349 break; 350 case BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED: 351 profile = BluetoothProfile.HEADSET_CLIENT; 352 break; 353 case BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED: 354 profile = BluetoothProfile.A2DP_SINK; 355 break; 356 case BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED: 357 profile = BluetoothProfile.PBAP_CLIENT; 358 break; 359 case BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED: 360 profile = BluetoothProfile.MAP_CLIENT; 361 break; 362 } 363 364 if (profile == -1) { 365 Log.e("Action does not match any given profiles " + action); 366 } 367 368 // The newer implementation will just post the Bundle with the literal event 369 // intead of the old implemenatation of posting BluetoothProfileConnectionStateChanged 370 // with the action inside of the Bundle. This makes for cleaner connection handling 371 // from test frameworks. Left the old implemenation in for backwards compatability. 372 373 // Post an event to Facade. 374 Bundle news = new Bundle(); 375 news.putInt("state", state); 376 news.putString("addr", device.getAddress()); 377 mEventFacade.postEvent(action, news); 378 379 news.putInt("profile", profile); 380 news.putString("action", action); 381 mEventFacade.postEvent("BluetoothProfileConnectionStateChanged", news); 382 } 383 } 384 385 /** 386 * Converts a given JSONArray to an ArrayList of Integers 387 * 388 * @param jsonArray the JSONArray to be converted 389 * @return <code>List<Integer></></code> the converted list of Integers 390 */ jsonArrayToIntegerList(JSONArray jsonArray)391 private List<Integer> jsonArrayToIntegerList(JSONArray jsonArray) throws JSONException { 392 if (jsonArray == null) { 393 return null; 394 } 395 List<Integer> intArray = new ArrayList<Integer>(); 396 for (int i = 0; i < jsonArray.length(); i++) { 397 intArray.add(jsonArray.getInt(i)); 398 } 399 return intArray; 400 401 } 402 403 @Rpc(description = "Start monitoring state changes for input device.") bluetoothStartConnectionStateChangeMonitor( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)404 public void bluetoothStartConnectionStateChangeMonitor( 405 @RpcParameter(name = "deviceID", 406 description = "Name or MAC address of a bluetooth device.") 407 String deviceID) { 408 if (!mDeviceMonitorList.contains(deviceID)) { 409 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID); 410 mService.registerReceiver(receiver, mA2dpStateChangeFilter); 411 mService.registerReceiver(receiver, mA2dpSinkStateChangeFilter); 412 mService.registerReceiver(receiver, mHidStateChangeFilter); 413 mService.registerReceiver(receiver, mHspStateChangeFilter); 414 mService.registerReceiver(receiver, mHfpClientStateChangeFilter); 415 mService.registerReceiver(receiver, mPbapClientStateChangeFilter); 416 mService.registerReceiver(receiver, mPanStateChangeFilter); 417 mService.registerReceiver(receiver, mMapClientStateChangeFilter); 418 mService.registerReceiver(receiver, mMapStateChangeFilter); 419 listeningDevices.put("StateChangeListener:" + deviceID, receiver); 420 } 421 } 422 423 /** 424 * Connect on all the profiles to the given Bluetooth device 425 * 426 * @param device The <code>BluetoothDevice</code> to connect to 427 * @param deviceID Name (String) of the device to connect to 428 */ connectProfile(BluetoothDevice device, String deviceID)429 private void connectProfile(BluetoothDevice device, String deviceID) { 430 mService.registerReceiver(mPairingHelper, mPairingFilter); 431 ParcelUuid[] deviceUuids = device.getUuids(); 432 Log.d("Device uuid is " + Arrays.toString(deviceUuids)); 433 if (deviceUuids == null) { 434 mEventFacade.postEvent("BluetoothProfileConnectionEvent", mBadNews); 435 } 436 Log.d("Connecting to " + device.getAlias()); 437 if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) { 438 mA2dpProfile.a2dpConnect(device); 439 } 440 if (BluetoothUuid.containsAnyUuid(BluetoothA2dpSinkFacade.SOURCE_UUIDS, deviceUuids)) { 441 mA2dpSinkProfile.a2dpSinkConnect(device); 442 } 443 if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) { 444 mHidProfile.hidConnect(device); 445 } 446 if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) { 447 mHspProfile.hspConnect(device); 448 } 449 if (BluetoothUuid.containsAnyUuid(BluetoothHfpClientFacade.UUIDS, deviceUuids)) { 450 mHfpClientProfile.hfpClientConnect(device); 451 } 452 if (BluetoothUuid.containsAnyUuid(BluetoothMapClientFacade.MAP_UUIDS, deviceUuids)) { 453 mMapClientProfile.mapClientConnect(device); 454 } 455 if (BluetoothUuid.containsAnyUuid(BluetoothPanFacade.UUIDS, deviceUuids)) { 456 mPanProfile.panConnect(device); 457 } 458 if (BluetoothUuid.containsAnyUuid(BluetoothPbapClientFacade.UUIDS, deviceUuids)) { 459 mPbapClientProfile.pbapClientConnect(device); 460 } 461 mService.unregisterReceiver(mPairingHelper); 462 } 463 464 /** 465 * Disconnect on all available profiles from the given device 466 * 467 * @param device The <code>BluetoothDevice</code> to disconnect from 468 * @param deviceID Name (String) of the device to disconnect from 469 */ disconnectProfiles(BluetoothDevice device, String deviceID)470 private void disconnectProfiles(BluetoothDevice device, String deviceID) { 471 Log.d("Disconnecting device " + device); 472 // Blindly disconnect all profiles. We may not have some of them connected so that will be a 473 // null op. 474 mA2dpProfile.a2dpDisconnect(device); 475 mA2dpSinkProfile.a2dpSinkDisconnect(device); 476 mHidProfile.hidDisconnect(device); 477 mHidDeviceProfile.hidDeviceDisconnect(device); 478 mHspProfile.hspDisconnect(device); 479 mHfpClientProfile.hfpClientDisconnect(device); 480 mPbapClientProfile.pbapClientDisconnect(device); 481 mPanProfile.panDisconnect(device); 482 mMapClientProfile.mapClientDisconnect(device); 483 } 484 485 /** 486 * Disconnect from specific profiles provided in the given List of profiles. 487 * 488 * @param device The {@link BluetoothDevice} to disconnect from 489 * @param deviceID Name/BDADDR (String) of the device to disconnect from 490 * @param profileIds The list of profiles we want to disconnect on. 491 */ disconnectProfiles(BluetoothDevice device, String deviceID, List<Integer> profileIds)492 private void disconnectProfiles(BluetoothDevice device, String deviceID, 493 List<Integer> profileIds) { 494 boolean result; 495 for (int profileId : profileIds) { 496 switch (profileId) { 497 case BluetoothProfile.A2DP_SINK: 498 mA2dpSinkProfile.a2dpSinkDisconnect(device); 499 break; 500 case BluetoothProfile.A2DP: 501 mA2dpProfile.a2dpDisconnect(device); 502 break; 503 case BluetoothProfile.HID_HOST: 504 mHidProfile.hidDisconnect(device); 505 break; 506 case BluetoothProfile.HID_DEVICE: 507 mHidDeviceProfile.hidDeviceDisconnect(device); 508 break; 509 case BluetoothProfile.HEADSET: 510 mHspProfile.hspDisconnect(device); 511 break; 512 case BluetoothProfile.HEADSET_CLIENT: 513 mHfpClientProfile.hfpClientDisconnect(device); 514 break; 515 case BluetoothProfile.PAN: 516 mPanProfile.panDisconnect(device); 517 break; 518 case BluetoothProfile.PBAP_CLIENT: 519 mPbapClientProfile.pbapClientDisconnect(device); 520 break; 521 case BluetoothProfile.MAP_CLIENT: 522 mMapClientProfile.mapDisconnect(device); 523 break; 524 default: 525 Log.d("Unknown Profile Id to disconnect from. Quitting"); 526 return; // returns on the first unknown profile it encounters. 527 } 528 } 529 } 530 531 @Rpc(description = "Start intercepting all bluetooth connection pop-ups.") bluetoothStartPairingHelper( @pcParametername = "autoConfirm", description = "Whether connection should be auto confirmed") @pcDefault"true") @pcOptional Boolean autoConfirm)532 public void bluetoothStartPairingHelper( 533 @RpcParameter(name = "autoConfirm", 534 description = "Whether connection should be auto confirmed") 535 @RpcDefault("true") @RpcOptional 536 Boolean autoConfirm) { 537 Log.d("Staring pairing helper"); 538 mPairingHelper.setAutoConfirm(autoConfirm); 539 mService.registerReceiver(mPairingHelper, mPairingFilter); 540 } 541 542 @Rpc(description = "Return a list of devices connected through bluetooth") bluetoothGetConnectedDevices()543 public List<BluetoothDevice> bluetoothGetConnectedDevices() { 544 ArrayList<BluetoothDevice> results = new ArrayList<BluetoothDevice>(); 545 for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) { 546 if (bd.isConnected()) { 547 results.add(bd); 548 } 549 } 550 return results; 551 } 552 553 /** 554 * Return a list of service UUIDS supported by the bonded device. 555 * @param macAddress the String mac address of the bonded device. 556 * 557 * @return the String list of supported UUIDS. 558 * @throws Exception 559 */ 560 @Rpc(description = "Return a list of service UUIDS supported by the bonded device") bluetoothGetBondedDeviceUuids( @pcParametername = "macAddress") String macAddress)561 public List<String> bluetoothGetBondedDeviceUuids( 562 @RpcParameter(name = "macAddress") String macAddress) throws Exception { 563 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 564 macAddress); 565 ArrayList<String> uuidStrings = new ArrayList<>(); 566 for (ParcelUuid parcelUuid : mDevice.getUuids()) { 567 uuidStrings.add(parcelUuid.toString()); 568 } 569 return uuidStrings; 570 } 571 572 @Rpc(description = "Return a list of devices connected through bluetooth LE") bluetoothGetConnectedLeDevices(Integer profile)573 public List<BluetoothDevice> bluetoothGetConnectedLeDevices(Integer profile) { 574 return mBluetoothManager.getConnectedDevices(profile); 575 } 576 577 @Rpc(description = "Bluetooth init Bond by Mac Address") bluetoothBond(@pcParametername = "macAddress") String macAddress)578 public boolean bluetoothBond(@RpcParameter(name = "macAddress") String macAddress) { 579 mContext.registerReceiver(new BondBroadcastReceiver(), 580 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); 581 return mBluetoothAdapter.getRemoteDevice(macAddress).createBond(); 582 } 583 584 @Rpc(description = "Bluetooth init LE Bond by Mac Address") bluetoothLeBond(@pcParametername = "macAddress") String macAddress)585 public boolean bluetoothLeBond(@RpcParameter(name = "macAddress") String macAddress) { 586 mContext.registerReceiver(new BondBroadcastReceiver(), 587 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); 588 return mBluetoothAdapter.getRemoteDevice(macAddress).createBond(BluetoothDevice.TRANSPORT_LE); 589 } 590 591 @Rpc(description = "Return true if a bluetooth device is connected.") bluetoothIsDeviceConnected(String deviceID)592 public Boolean bluetoothIsDeviceConnected(String deviceID) { 593 for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) { 594 if (BluetoothFacade.deviceMatch(bd, deviceID)) { 595 return bd.isConnected(); 596 } 597 } 598 return false; 599 } 600 601 /** 602 * Generates the local Out of Band data for the given transport. 603 */ 604 @Rpc(description = "Generate Out of Band data for OOB Pairing.") bluetoothGenerateLocalOobData(@pcParametername = "transport") String transport)605 public void bluetoothGenerateLocalOobData(@RpcParameter(name = "transport") String transport) { 606 Log.d("bluetoothGenerateLocalOobData(" + transport + ")"); 607 mBluetoothAdapter.generateLocalOobData(Integer.parseInt(transport), 608 mContext.getMainExecutor(), mGenerateOobDataCallback); 609 610 } 611 hexStringToByteArray(String s)612 private static byte[] hexStringToByteArray(String s) { 613 if (s == null) { 614 throw new IllegalArgumentException("Hex String must not be null!"); 615 } 616 int len = s.length(); 617 if ((len % 2) != 0 || len < 1) { // Multiple of 2 or empty 618 throw new IllegalArgumentException("Hex String must be an even number > 0"); 619 } 620 byte[] data = new byte[len / 2]; 621 for (int i = 0; i < len; i += 2) { 622 data[i / 2] = (byte) ((byte) (Character.digit(s.charAt(i), 16) << 4) 623 + (byte) Character.digit(s.charAt(i + 1), 16)); 624 } 625 return data; 626 } 627 toHexString(byte[] a)628 private static String toHexString(byte[] a) { 629 if (a == null) return null; 630 StringBuilder builder = new StringBuilder(a.length * 2); 631 for (byte b : a) { 632 builder.append(String.format("%02x", b)); 633 } 634 return builder.toString(); 635 } 636 637 /** 638 * Bond to a device using Out of Band Data over LE transport. Note that there is a distinction 639 * between the address with type supplied in the oob data and the address and type of the 640 * BluetoothDevice object. 641 * 642 * @param oobDataAddress is the MAC address to be used in the oob data 643 * @param oobDataAddressType is the BluetoothDevice.AddressType for the oob data MAC address 644 * @param transport String "1", "2", "3" to match TRANSPORT_* 645 * @param c Hex String of the 16 octet confirmation 646 * @param r Hex String of the 16 octet randomizer 647 * @param address String representation of MAC address for the BluetoothDevice object 648 * @param addressType the BluetoothDevice.AddressType for the BluetoothDevice object 649 */ 650 @Rpc(description = "Creates and Out of Band LE bond.") bluetoothCreateLeBondOutOfBand( @pcParametername = "oobDataAddress") String oobDataAddress, @RpcParameter(name = "oobDataAddressType") Integer oobDataAddressType, @RpcParameter(name = "c") String c, @RpcParameter(name = "r") String r, @RpcParameter(name = "address") String address, @RpcParameter(name = "addressType") @RpcDefault("1") Integer addressType)651 public boolean bluetoothCreateLeBondOutOfBand( 652 @RpcParameter(name = "oobDataAddress") String oobDataAddress, 653 @RpcParameter(name = "oobDataAddressType") Integer oobDataAddressType, 654 @RpcParameter(name = "c") String c, @RpcParameter(name = "r") String r, 655 @RpcParameter(name = "address") String address, 656 @RpcParameter(name = "addressType") @RpcDefault("1") Integer addressType) { 657 Log.d("bluetoothCreateLeBondOutOfBand(" + address + ", " + addressType + "," + c + ", " 658 + r + ")"); 659 BluetoothDevice remoteDevice = mBluetoothAdapter.getRemoteLeDevice(address, addressType); 660 byte[] addressBytes = new byte[7]; 661 int i = 0; 662 for (String s : oobDataAddress.split(":")) { 663 addressBytes[i] = hexStringToByteArray(s)[0]; 664 i++; 665 } 666 667 // Inserts the oob address type if one is provided 668 if (oobDataAddressType == BluetoothDevice.ADDRESS_TYPE_PUBLIC 669 || oobDataAddressType == BluetoothDevice.ADDRESS_TYPE_RANDOM) { 670 addressBytes[i] = oobDataAddressType.byteValue(); 671 } 672 673 OobData p192 = null; 674 OobData p256 = new OobData.LeBuilder(hexStringToByteArray(c), 675 addressBytes, OobData.LE_DEVICE_ROLE_BOTH_PREFER_CENTRAL) 676 .setRandomizerHash(hexStringToByteArray(r)) 677 .build(); 678 mContext.registerReceiver(new BondBroadcastReceiver(), 679 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); 680 return remoteDevice.createBondOutOfBand(BluetoothDevice.TRANSPORT_LE, p192, p256); 681 } 682 683 private class BondBroadcastReceiver extends BroadcastReceiver { 684 @Override onReceive(Context context, Intent intent)685 public void onReceive(Context context, Intent intent) { 686 Log.d("BondBroadcastReceiver onReceive(" + context + ", " + intent + ")"); 687 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 688 BluetoothDevice.BOND_NONE); 689 if (state == BluetoothDevice.BOND_BONDED) { 690 Bundle event = new Bundle(); 691 event.putBoolean("bonded_state", state == BluetoothDevice.BOND_BONDED); 692 mEventFacade.postEvent("Bonded", event); 693 mContext.unregisterReceiver(this); 694 } else if (state == BluetoothDevice.BOND_NONE) { 695 Bundle event = new Bundle(); 696 event.putBoolean("bonded_state", state == BluetoothDevice.BOND_BONDED); 697 mEventFacade.postEvent("Unbonded", event); 698 mContext.unregisterReceiver(this); 699 } 700 } 701 } 702 703 @Rpc(description = "Return list of connected bluetooth devices over a profile", 704 returns = "List of devices connected over the profile") bluetoothGetConnectedDevicesOnProfile( @pcParametername = "profileId", description = "profileId same as BluetoothProfile") Integer profileId)705 public List<BluetoothDevice> bluetoothGetConnectedDevicesOnProfile( 706 @RpcParameter(name = "profileId", 707 description = "profileId same as BluetoothProfile") 708 Integer profileId) { 709 BluetoothProfile profile = null; 710 switch (profileId) { 711 case BluetoothProfile.A2DP_SINK: 712 return mA2dpSinkProfile.bluetoothA2dpSinkGetConnectedDevices(); 713 case BluetoothProfile.HEADSET_CLIENT: 714 return mHfpClientProfile.bluetoothHfpClientGetConnectedDevices(); 715 case BluetoothProfile.PBAP_CLIENT: 716 return mPbapClientProfile.bluetoothPbapClientGetConnectedDevices(); 717 case BluetoothProfile.MAP_CLIENT: 718 return mMapClientProfile.bluetoothMapClientGetConnectedDevices(); 719 case BluetoothProfile.HID_HOST: 720 return mHidProfile.bluetoothHidGetConnectedDevices(); 721 default: 722 Log.w("Profile id " + profileId + " is not yet supported."); 723 return new ArrayList<BluetoothDevice>(); 724 } 725 } 726 727 @Rpc(description = "Connect to a specified device once it's discovered.", 728 returns = "Whether discovery started successfully.") bluetoothDiscoverAndConnect( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)729 public Boolean bluetoothDiscoverAndConnect( 730 @RpcParameter(name = "deviceID", 731 description = "Name or MAC address of a bluetooth device.") 732 String deviceID) { 733 mBluetoothAdapter.cancelDiscovery(); 734 if (listeningDevices.containsKey(deviceID)) { 735 Log.d("This device is already in the process of discovery and connecting."); 736 return true; 737 } 738 DiscoverConnectReceiver receiver = new DiscoverConnectReceiver(deviceID); 739 listeningDevices.put("Connect" + deviceID, receiver); 740 mService.registerReceiver(receiver, mDiscoverConnectFilter); 741 return mBluetoothAdapter.startDiscovery(); 742 } 743 744 @Rpc(description = "Bond to a specified device once it's discovered.", 745 returns = "Whether discovery started successfully. ") bluetoothDiscoverAndBond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)746 public Boolean bluetoothDiscoverAndBond( 747 @RpcParameter(name = "deviceID", 748 description = "Name or MAC address of a bluetooth device.") 749 String deviceID) { 750 mBluetoothAdapter.cancelDiscovery(); 751 if (listeningDevices.containsKey(deviceID)) { 752 Log.d("This device is already in the process of discovery and bonding."); 753 return true; 754 } 755 if (BluetoothFacade.deviceExists(mBluetoothAdapter.getBondedDevices(), deviceID)) { 756 Log.d("Device " + deviceID + " is already bonded."); 757 mEventFacade.postEvent("Bond" + deviceID, mGoodNews); 758 return true; 759 } 760 DiscoverBondReceiver receiver = new DiscoverBondReceiver(deviceID); 761 if (listeningDevices.containsKey("Bond" + deviceID)) { 762 mService.unregisterReceiver(listeningDevices.remove("Bond" + deviceID)); 763 } 764 listeningDevices.put("Bond" + deviceID, receiver); 765 mService.registerReceiver(receiver, mBondFilter); 766 Log.d("Start discovery for bonding."); 767 return mBluetoothAdapter.startDiscovery(); 768 } 769 770 @Rpc(description = "Unbond a device.", 771 returns = "Whether the device was successfully unbonded.") bluetoothUnbond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)772 public Boolean bluetoothUnbond( 773 @RpcParameter(name = "deviceID", 774 description = "Name or MAC address of a bluetooth device.") 775 String deviceID) throws Exception { 776 // We don't want to crash the test if the test passes an address that cannot be found. 777 try { 778 BluetoothDevice mDevice = BluetoothFacade.getDevice( 779 mBluetoothAdapter.getBondedDevices(), deviceID); 780 mContext.registerReceiver(new BondBroadcastReceiver(), 781 new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); 782 return mDevice.removeBond(); 783 } catch (Exception e) { 784 Log.d("Failed to find the device by deviceId"); 785 return false; 786 } 787 } 788 789 @Rpc(description = "Connect to a device that is already bonded.") bluetoothConnectBonded( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)790 public void bluetoothConnectBonded( 791 @RpcParameter(name = "deviceID", 792 description = "Name or MAC address of a bluetooth device.") 793 String deviceID) throws Exception { 794 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 795 deviceID); 796 connectProfile(mDevice, deviceID); 797 } 798 799 @Rpc(description = "Disconnect from a device that is already connected.") bluetoothDisconnectConnected( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)800 public void bluetoothDisconnectConnected( 801 @RpcParameter(name = "deviceID", 802 description = "Name or MAC address of a bluetooth device.") 803 String deviceID) throws Exception { 804 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 805 deviceID); 806 disconnectProfiles(mDevice, deviceID); 807 } 808 809 @Rpc(description = "Disconnect on a profile from a device that is already connected.") bluetoothDisconnectConnectedProfile( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "profileSet", description = "List of profiles to disconnect from.") JSONArray profileSet )810 public void bluetoothDisconnectConnectedProfile( 811 @RpcParameter(name = "deviceID", 812 description = "Name or MAC address of a bluetooth device.") 813 String deviceID, 814 @RpcParameter(name = "profileSet", 815 description = "List of profiles to disconnect from.") 816 JSONArray profileSet 817 ) throws Exception { 818 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 819 deviceID); 820 disconnectProfiles(mDevice, deviceID, jsonArrayToIntegerList(profileSet)); 821 } 822 823 @Rpc(description = "Change permissions for a profile.") bluetoothChangeProfileAccessPermission( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID, @RpcParameter(name = "profileID", description = "Number of Profile to change access permission") Integer profileID, @RpcParameter(name = "access", description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected") Integer access )824 public void bluetoothChangeProfileAccessPermission( 825 @RpcParameter(name = "deviceID", 826 description = "Name or MAC address of a bluetooth device.") 827 String deviceID, 828 @RpcParameter(name = "profileID", 829 description = "Number of Profile to change access permission") 830 Integer profileID, 831 @RpcParameter(name = "access", 832 description = "Access level 0 = Unknown, 1 = Allowed, 2 = Rejected") 833 Integer access 834 ) throws Exception { 835 if (access < 0 || access > 2) { 836 Log.w("Unsupported access level."); 837 return; 838 } 839 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 840 deviceID); 841 switch (profileID) { 842 case BluetoothProfile.PBAP: 843 mDevice.setPhonebookAccessPermission(access); 844 break; 845 default: 846 Log.w("Unsupported profile access change."); 847 } 848 } 849 850 851 @Override shutdown()852 public void shutdown() { 853 for (BroadcastReceiver receiver : listeningDevices.values()) { 854 try { 855 mService.unregisterReceiver(receiver); 856 } catch (IllegalArgumentException ex) { 857 Log.e("Failed to unregister " + ex); 858 } 859 } 860 listeningDevices.clear(); 861 mService.unregisterReceiver(mPairingHelper); 862 } 863 } 864 865