1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.googlecode.android_scripting.facade.bluetooth; 18 19 import java.util.ArrayList; 20 import java.util.Collections; 21 import java.util.HashMap; 22 import java.util.List; 23 import java.util.Map; 24 25 import android.app.Service; 26 import android.bluetooth.BluetoothA2dp; 27 import android.bluetooth.BluetoothA2dpSink; 28 import android.bluetooth.BluetoothAdapter; 29 import android.bluetooth.BluetoothDevice; 30 import android.bluetooth.BluetoothHeadset; 31 import android.bluetooth.BluetoothHeadsetClient; 32 import android.bluetooth.BluetoothInputDevice; 33 import android.bluetooth.BluetoothUuid; 34 import android.content.BroadcastReceiver; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.IntentFilter; 38 import android.os.Bundle; 39 import android.os.ParcelUuid; 40 41 import com.googlecode.android_scripting.Log; 42 import com.googlecode.android_scripting.facade.EventFacade; 43 import com.googlecode.android_scripting.facade.FacadeManager; 44 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 45 import com.googlecode.android_scripting.rpc.Rpc; 46 import com.googlecode.android_scripting.rpc.RpcParameter; 47 48 public class BluetoothConnectionFacade extends RpcReceiver { 49 50 private final Service mService; 51 private final BluetoothAdapter mBluetoothAdapter; 52 private final BluetoothPairingHelper mPairingHelper; 53 private final Map<String, BroadcastReceiver> listeningDevices; 54 private final EventFacade mEventFacade; 55 56 private final IntentFilter mDiscoverConnectFilter; 57 private final IntentFilter mPairingFilter; 58 private final IntentFilter mBondFilter; 59 private final IntentFilter mA2dpStateChangeFilter; 60 private final IntentFilter mA2dpSinkStateChangeFilter; 61 private final IntentFilter mHidStateChangeFilter; 62 private final IntentFilter mHspStateChangeFilter; 63 private final IntentFilter mHfpClientStateChangeFilter; 64 65 private final Bundle mGoodNews; 66 private final Bundle mBadNews; 67 68 private BluetoothA2dpFacade mA2dpProfile; 69 private BluetoothA2dpSinkFacade mA2dpSinkProfile; 70 private BluetoothHidFacade mHidProfile; 71 private BluetoothHspFacade mHspProfile; 72 private BluetoothHfpClientFacade mHfpClientProfile; 73 BluetoothConnectionFacade(FacadeManager manager)74 public BluetoothConnectionFacade(FacadeManager manager) { 75 super(manager); 76 mService = manager.getService(); 77 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 78 mPairingHelper = new BluetoothPairingHelper(); 79 // Use a synchronized map to avoid racing problems 80 listeningDevices = Collections.synchronizedMap(new HashMap<String, BroadcastReceiver>()); 81 82 mEventFacade = manager.getReceiver(EventFacade.class); 83 mA2dpProfile = manager.getReceiver(BluetoothA2dpFacade.class); 84 mA2dpSinkProfile = manager.getReceiver(BluetoothA2dpSinkFacade.class); 85 mHidProfile = manager.getReceiver(BluetoothHidFacade.class); 86 mHspProfile = manager.getReceiver(BluetoothHspFacade.class); 87 mHfpClientProfile = manager.getReceiver(BluetoothHfpClientFacade.class); 88 89 mDiscoverConnectFilter = new IntentFilter(BluetoothDevice.ACTION_FOUND); 90 mDiscoverConnectFilter.addAction(BluetoothDevice.ACTION_UUID); 91 mDiscoverConnectFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 92 93 mPairingFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 94 mPairingFilter.addAction(BluetoothDevice.ACTION_CONNECTION_ACCESS_REQUEST); 95 mPairingFilter.setPriority(999); 96 97 mBondFilter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 98 mBondFilter.addAction(BluetoothDevice.ACTION_FOUND); 99 mBondFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); 100 101 mA2dpStateChangeFilter = new IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); 102 mA2dpSinkStateChangeFilter = 103 new IntentFilter(BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); 104 mHidStateChangeFilter = 105 new IntentFilter(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED); 106 mHspStateChangeFilter = new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); 107 mHfpClientStateChangeFilter = 108 new IntentFilter(BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED); 109 110 mGoodNews = new Bundle(); 111 mGoodNews.putBoolean("Status", true); 112 mBadNews = new Bundle(); 113 mBadNews.putBoolean("Status", false); 114 } 115 unregisterCachedListener(String listenerId)116 private void unregisterCachedListener(String listenerId) { 117 BroadcastReceiver listener = listeningDevices.remove(listenerId); 118 if (listener != null) { 119 mService.unregisterReceiver(listener); 120 } 121 } 122 123 /** 124 * Connect to a specific device upon its discovery 125 */ 126 public class DiscoverConnectReceiver extends BroadcastReceiver { 127 private final String mDeviceID; 128 private BluetoothDevice mDevice; 129 130 /** 131 * Constructor 132 * 133 * @param deviceID Either the device alias name or mac address. 134 * @param bond If true, bond the device only. 135 */ DiscoverConnectReceiver(String deviceID)136 public DiscoverConnectReceiver(String deviceID) { 137 super(); 138 mDeviceID = deviceID; 139 } 140 141 @Override onReceive(Context context, Intent intent)142 public void onReceive(Context context, Intent intent) { 143 String action = intent.getAction(); 144 // The specified device is found. 145 if (action.equals(BluetoothDevice.ACTION_FOUND)) { 146 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 147 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 148 Log.d("Found device " + device.getAliasName() + " for connection."); 149 mBluetoothAdapter.cancelDiscovery(); 150 mDevice = device; 151 } 152 // After discovery stops. 153 } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { 154 if (mDevice == null) { 155 Log.d("Device " + mDeviceID + " not discovered."); 156 mEventFacade.postEvent("Bond" + mDeviceID, mBadNews); 157 return; 158 } 159 boolean status = mDevice.fetchUuidsWithSdp(); 160 Log.d("Initiated ACL connection: " + status); 161 } else if (action.equals(BluetoothDevice.ACTION_UUID)) { 162 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 163 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 164 Log.d("Initiating connections."); 165 connectProfile(device, mDeviceID); 166 mService.unregisterReceiver(listeningDevices.remove("Connect" + mDeviceID)); 167 } 168 } 169 } 170 } 171 172 /** 173 * Connect to a specific device upon its discovery 174 */ 175 public class DiscoverBondReceiver extends BroadcastReceiver { 176 private final String mDeviceID; 177 private BluetoothDevice mDevice = null; 178 private boolean started = false; 179 180 /** 181 * Constructor 182 * 183 * @param deviceID Either the device alias name or Mac address. 184 */ DiscoverBondReceiver(String deviceID)185 public DiscoverBondReceiver(String deviceID) { 186 super(); 187 mDeviceID = deviceID; 188 } 189 190 @Override onReceive(Context context, Intent intent)191 public void onReceive(Context context, Intent intent) { 192 String action = intent.getAction(); 193 // The specified device is found. 194 if (action.equals(BluetoothDevice.ACTION_FOUND)) { 195 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 196 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 197 Log.d("Found device " + device.getAliasName() + " for connection."); 198 mBluetoothAdapter.cancelDiscovery(); 199 mDevice = device; 200 } 201 // After discovery stops. 202 } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) { 203 if (mDevice == null) { 204 Log.d("Device " + mDeviceID + " was not discovered."); 205 mEventFacade.postEvent("Bond", mBadNews); 206 return; 207 } 208 // Attempt to initiate bonding. 209 if (!started) { 210 Log.d("Bond with " + mDevice.getAliasName()); 211 if (mDevice.createBond()) { 212 started = true; 213 Log.d("Bonding started."); 214 } else { 215 Log.e("Failed to bond with " + mDevice.getAliasName()); 216 mEventFacade.postEvent("Bond", mBadNews); 217 mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID)); 218 } 219 } 220 } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { 221 Log.d("Bond state changing."); 222 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 223 if (BluetoothFacade.deviceMatch(device, mDeviceID)) { 224 int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); 225 Log.d("New state is " + state); 226 if (state == BluetoothDevice.BOND_BONDED) { 227 Log.d("Bonding with " + mDeviceID + " successful."); 228 mEventFacade.postEvent("Bond" + mDeviceID, mGoodNews); 229 mService.unregisterReceiver(listeningDevices.remove("Bond" + mDeviceID)); 230 } 231 } 232 } 233 } 234 } 235 236 public class ConnectStateChangeReceiver extends BroadcastReceiver { 237 private final String mDeviceID; 238 ConnectStateChangeReceiver(String deviceID)239 public ConnectStateChangeReceiver(String deviceID) { 240 mDeviceID = deviceID; 241 } 242 243 @Override onReceive(Context context, Intent intent)244 public void onReceive(Context context, Intent intent) { 245 String action = intent.getAction(); 246 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 247 // Check if received the specified device 248 if (!BluetoothFacade.deviceMatch(device, mDeviceID)) { 249 return; 250 } 251 if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { 252 int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); 253 if (state == BluetoothA2dp.STATE_CONNECTED) { 254 Bundle a2dpGoodNews = (Bundle) mGoodNews.clone(); 255 a2dpGoodNews.putString("Type", "a2dp"); 256 mEventFacade.postEvent("A2dpConnect" + mDeviceID, a2dpGoodNews); 257 unregisterCachedListener("A2dpConnecting" + mDeviceID); 258 } 259 } else if (action.equals(BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED)) { 260 int state = intent.getIntExtra(BluetoothInputDevice.EXTRA_STATE, -1); 261 if (state == BluetoothInputDevice.STATE_CONNECTED) { 262 mEventFacade.postEvent("HidConnect" + mDeviceID, mGoodNews); 263 unregisterCachedListener("HidConnecting" + mDeviceID); 264 } 265 } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { 266 int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -1); 267 if (state == BluetoothHeadset.STATE_CONNECTED) { 268 mEventFacade.postEvent("HspConnect" + mDeviceID, mGoodNews); 269 unregisterCachedListener("HspConnecting" + mDeviceID); 270 } 271 } 272 } 273 } 274 connectProfile(BluetoothDevice device, String deviceID)275 private void connectProfile(BluetoothDevice device, String deviceID) { 276 mService.registerReceiver(mPairingHelper, mPairingFilter); 277 ParcelUuid[] deviceUuids = device.getUuids(); 278 Log.d("Device uuid is " + deviceUuids); 279 if (deviceUuids == null) { 280 mEventFacade.postEvent("BluetoothProfileConnectionEvent", mBadNews); 281 } 282 Log.d("Connecting to " + device.getAliasName()); 283 if (BluetoothUuid.containsAnyUuid(BluetoothA2dpFacade.SINK_UUIDS, deviceUuids)) { 284 boolean status = mA2dpProfile.a2dpConnect(device); 285 if (status) { 286 Log.d("Connecting A2dp..."); 287 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID); 288 mService.registerReceiver(receiver, mA2dpStateChangeFilter); 289 listeningDevices.put("A2dpConnecting" + deviceID, receiver); 290 } else { 291 Log.d("Failed starting A2dp connection."); 292 Bundle a2dpBadNews = (Bundle) mBadNews.clone(); 293 a2dpBadNews.putString("Type", "a2dp"); 294 mEventFacade.postEvent("Connect", a2dpBadNews); 295 } 296 } 297 if (BluetoothUuid.containsAnyUuid(BluetoothA2dpSinkFacade.SOURCE_UUIDS, deviceUuids)) { 298 boolean status = mA2dpSinkProfile.a2dpSinkConnect(device); 299 if (status) { 300 Log.d("Connecting A2dp Sink..."); 301 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID); 302 mService.registerReceiver(receiver, mA2dpSinkStateChangeFilter); 303 listeningDevices.put("A2dpSinkConnecting" + deviceID, receiver); 304 } else { 305 Log.d("Failed starting A2dp Sink connection."); 306 Bundle a2dpSinkBadNews = (Bundle) mBadNews.clone(); 307 a2dpSinkBadNews.putString("Type", "a2dpsink"); 308 mEventFacade.postEvent("Connect", a2dpSinkBadNews); 309 } 310 } 311 if (BluetoothUuid.containsAnyUuid(BluetoothHidFacade.UUIDS, deviceUuids)) { 312 boolean status = mHidProfile.hidConnect(device); 313 if (status) { 314 Log.d("Connecting Hid..."); 315 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID); 316 mService.registerReceiver(receiver, mHidStateChangeFilter); 317 listeningDevices.put("HidConnecting" + deviceID, receiver); 318 } else { 319 Log.d("Failed starting Hid connection."); 320 mEventFacade.postEvent("HidConnect" + deviceID, mBadNews); 321 } 322 } 323 if (BluetoothUuid.containsAnyUuid(BluetoothHspFacade.UUIDS, deviceUuids)) { 324 boolean status = mHspProfile.hspConnect(device); 325 if (status) { 326 Log.d("Connecting Hsp..."); 327 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID); 328 mService.registerReceiver(receiver, mHspStateChangeFilter); 329 listeningDevices.put("HspConnecting" + deviceID, receiver); 330 } else { 331 Log.d("Failed starting Hsp connection."); 332 mEventFacade.postEvent("HspConnect" + deviceID, mBadNews); 333 } 334 } 335 if (BluetoothUuid.containsAnyUuid(BluetoothHfpClientFacade.UUIDS, deviceUuids)) { 336 boolean status = mHfpClientProfile.hfpClientConnect(device); 337 if (status) { 338 Log.d("Connecting HFP Client ..."); 339 ConnectStateChangeReceiver receiver = new ConnectStateChangeReceiver(deviceID); 340 mService.registerReceiver(receiver, mHfpClientStateChangeFilter); 341 listeningDevices.put("HfpClientConnecting" + deviceID, receiver); 342 } else { 343 Log.d("Failed starting Hfp Client connection."); 344 mEventFacade.postEvent("HfpClientConnect" + deviceID, mBadNews); 345 } 346 } 347 mService.unregisterReceiver(mPairingHelper); 348 } 349 disconnectProfiles(BluetoothDevice device, String deviceID)350 private void disconnectProfiles(BluetoothDevice device, String deviceID) { 351 Log.d("Disconnecting device " + device); 352 // Blindly disconnect all profiles. We may not have some of them connected so that will be a 353 // null op. 354 mA2dpProfile.a2dpDisconnect(device); 355 mA2dpSinkProfile.a2dpSinkDisconnect(device); 356 mHidProfile.hidDisconnect(device); 357 mHspProfile.hspDisconnect(device); 358 mHfpClientProfile.hfpClientDisconnect(device); 359 } 360 361 @Rpc(description = "Start intercepting all bluetooth connection pop-ups.") bluetoothStartPairingHelper()362 public void bluetoothStartPairingHelper() { 363 mService.registerReceiver(mPairingHelper, mPairingFilter); 364 } 365 366 @Rpc(description = "Return a list of devices connected through bluetooth") bluetoothGetConnectedDevices()367 public List<BluetoothDevice> bluetoothGetConnectedDevices() { 368 ArrayList<BluetoothDevice> results = new ArrayList<BluetoothDevice>(); 369 for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) { 370 if (bd.isConnected()) { 371 results.add(bd); 372 } 373 } 374 return results; 375 } 376 377 @Rpc(description = "Return true if a bluetooth device is connected.") bluetoothIsDeviceConnected(String deviceID)378 public Boolean bluetoothIsDeviceConnected(String deviceID) { 379 for (BluetoothDevice bd : mBluetoothAdapter.getBondedDevices()) { 380 if (BluetoothFacade.deviceMatch(bd, deviceID)) { 381 return bd.isConnected(); 382 } 383 } 384 return false; 385 } 386 387 @Rpc(description = "Connect to a specified device once it's discovered.", 388 returns = "Whether discovery started successfully.") bluetoothDiscoverAndConnect( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)389 public Boolean bluetoothDiscoverAndConnect( 390 @RpcParameter(name = "deviceID", 391 description = "Name or MAC address of a bluetooth device.") 392 String deviceID) { 393 mBluetoothAdapter.cancelDiscovery(); 394 if (listeningDevices.containsKey(deviceID)) { 395 Log.d("This device is already in the process of discovery and connecting."); 396 return true; 397 } 398 DiscoverConnectReceiver receiver = new DiscoverConnectReceiver(deviceID); 399 listeningDevices.put("Connect" + deviceID, receiver); 400 mService.registerReceiver(receiver, mDiscoverConnectFilter); 401 return mBluetoothAdapter.startDiscovery(); 402 } 403 404 @Rpc(description = "Bond to a specified device once it's discovered.", 405 returns = "Whether discovery started successfully. ") bluetoothDiscoverAndBond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)406 public Boolean bluetoothDiscoverAndBond( 407 @RpcParameter(name = "deviceID", 408 description = "Name or MAC address of a bluetooth device.") 409 String deviceID) { 410 mBluetoothAdapter.cancelDiscovery(); 411 if (listeningDevices.containsKey(deviceID)) { 412 Log.d("This device is already in the process of discovery and bonding."); 413 return true; 414 } 415 if (BluetoothFacade.deviceExists(mBluetoothAdapter.getBondedDevices(), deviceID)) { 416 Log.d("Device " + deviceID + " is already bonded."); 417 mEventFacade.postEvent("Bond" + deviceID, mGoodNews); 418 return true; 419 } 420 DiscoverBondReceiver receiver = new DiscoverBondReceiver(deviceID); 421 if (listeningDevices.containsKey("Bond" + deviceID)) { 422 mService.unregisterReceiver(listeningDevices.remove("Bond" + deviceID)); 423 } 424 listeningDevices.put("Bond" + deviceID, receiver); 425 mService.registerReceiver(receiver, mBondFilter); 426 Log.d("Start discovery for bonding."); 427 return mBluetoothAdapter.startDiscovery(); 428 } 429 430 @Rpc(description = "Unbond a device.", 431 returns = "Whether the device was successfully unbonded.") bluetoothUnbond( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)432 public Boolean bluetoothUnbond( 433 @RpcParameter(name = "deviceID", 434 description = "Name or MAC address of a bluetooth device.") 435 String deviceID) throws Exception { 436 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 437 deviceID); 438 return mDevice.removeBond(); 439 } 440 441 @Rpc(description = "Connect to a device that is already bonded.") bluetoothConnectBonded( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)442 public void bluetoothConnectBonded( 443 @RpcParameter(name = "deviceID", 444 description = "Name or MAC address of a bluetooth device.") 445 String deviceID) throws Exception { 446 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 447 deviceID); 448 connectProfile(mDevice, deviceID); 449 } 450 451 // TODO: Split the disconnect RPC by profiles as well for granular control over the ACL 452 @Rpc(description = "Disconnect from a device that is already connected.") bluetoothDisconnectConnected( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)453 public void bluetoothDisconnectConnected( 454 @RpcParameter(name = "deviceID", 455 description = "Name or MAC address of a bluetooth device.") 456 String deviceID) throws Exception { 457 BluetoothDevice mDevice = BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), 458 deviceID); 459 disconnectProfiles(mDevice, deviceID); 460 } 461 462 @Override shutdown()463 public void shutdown() { 464 for(BroadcastReceiver receiver : listeningDevices.values()) { 465 mService.unregisterReceiver(receiver); 466 } 467 listeningDevices.clear(); 468 mService.unregisterReceiver(mPairingHelper); 469 } 470 } 471