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 android.app.Service; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothServerSocket; 23 import android.bluetooth.BluetoothSocket; 24 import android.content.IntentFilter; 25 import android.os.ParcelFileDescriptor; 26 27 import com.googlecode.android_scripting.Log; 28 import com.googlecode.android_scripting.facade.EventFacade; 29 import com.googlecode.android_scripting.facade.bluetooth.BluetoothPairingHelper; 30 import com.googlecode.android_scripting.facade.FacadeManager; 31 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 32 import com.googlecode.android_scripting.rpc.Rpc; 33 import com.googlecode.android_scripting.rpc.RpcDefault; 34 import com.googlecode.android_scripting.rpc.RpcOptional; 35 import com.googlecode.android_scripting.rpc.RpcParameter; 36 37 import java.io.BufferedReader; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.io.InputStreamReader; 41 import java.io.OutputStream; 42 import java.util.HashMap; 43 import java.util.Map; 44 import java.util.UUID; 45 import java.lang.reflect.Field; 46 import java.lang.Thread; 47 48 import org.apache.commons.codec.binary.Base64Codec; 49 50 /** 51 * Bluetooth functions. 52 * 53 */ 54 // Discovery functions added by Eden Sayag 55 56 public class BluetoothRfcommFacade extends RpcReceiver { 57 58 // UUID for SL4A. 59 private static final String DEFAULT_UUID = "457807c0-4897-11df-9879-0800200c9a66"; 60 private static final String SDP_NAME = "SL4A"; 61 private final Service mService; 62 private final BluetoothAdapter mBluetoothAdapter; 63 private Map<String, BluetoothConnection> 64 connections = new HashMap<String, BluetoothConnection>(); 65 private final EventFacade mEventFacade; 66 private ConnectThread mConnectThread; 67 private AcceptThread mAcceptThread; 68 BluetoothRfcommFacade(FacadeManager manager)69 public BluetoothRfcommFacade(FacadeManager manager) { 70 super(manager); 71 mEventFacade = manager.getReceiver(EventFacade.class); 72 mService = manager.getService(); 73 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 74 } 75 getConnection(String connID)76 private BluetoothConnection getConnection(String connID) throws IOException { 77 BluetoothConnection conn = null; 78 if (connID.trim().length() > 0) { 79 conn = connections.get(connID); 80 } else if (connections.size() == 1) { 81 conn = (BluetoothConnection) connections.values().toArray()[0]; 82 } 83 if (conn == null) { 84 throw new IOException("Bluetooth connection not established."); 85 } 86 return conn; 87 } 88 addConnection(BluetoothConnection conn)89 private String addConnection(BluetoothConnection conn) { 90 String uuid = UUID.randomUUID().toString(); 91 connections.put(uuid, conn); 92 conn.setUUID(uuid); 93 return uuid; 94 } 95 96 @Rpc(description = "Begins a thread initiate an Rfcomm connection over Bluetooth. ") bluetoothRfcommBeginConnectThread( @pcParametername = "address", description = "The mac address of the device to connect to.") String address, @RpcParameter(name = "uuid", description = "The UUID passed here must match the UUID used by the server device.") @RpcDefault(DEFAULT_UUID) String uuid)97 public void bluetoothRfcommBeginConnectThread( 98 @RpcParameter(name = "address", description = "The mac address of the device to connect to.") 99 String address, 100 @RpcParameter(name = "uuid", 101 description = "The UUID passed here must match the UUID used by the server device.") 102 @RpcDefault(DEFAULT_UUID) 103 String uuid) 104 throws IOException { 105 BluetoothDevice mDevice; 106 mDevice = mBluetoothAdapter.getRemoteDevice(address); 107 ConnectThread connectThread = new ConnectThread(mDevice, uuid); 108 connectThread.start(); 109 mConnectThread = connectThread; 110 } 111 112 @Rpc(description = "Kill thread") bluetoothRfcommKillConnThread()113 public void bluetoothRfcommKillConnThread() { 114 try { 115 mConnectThread.cancel(); 116 mConnectThread.join(5000); 117 } catch (InterruptedException e) { 118 Log.e("Interrupted Exception: " + e.toString()); 119 } 120 } 121 122 /** 123 * Closes an active Rfcomm Client socket 124 */ 125 @Rpc(description = "Close an active Rfcomm Client socket") bluetoothRfcommEndConnectThread()126 public void bluetoothRfcommEndConnectThread() 127 throws IOException { 128 mConnectThread.cancel(); 129 } 130 131 /** 132 * Closes an active Rfcomm Server socket 133 */ 134 @Rpc(description = "Close an active Rfcomm Server socket") bluetoothRfcommEndAcceptThread()135 public void bluetoothRfcommEndAcceptThread() 136 throws IOException { 137 mAcceptThread.cancel(); 138 } 139 140 @Rpc(description = "Returns active Bluetooth connections.") bluetoothRfcommActiveConnections()141 public Map<String, String> bluetoothRfcommActiveConnections() { 142 Map<String, String> out = new HashMap<String, String>(); 143 for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) { 144 if (entry.getValue().isConnected()) { 145 out.put(entry.getKey(), entry.getValue().getRemoteBluetoothAddress()); 146 } 147 } 148 return out; 149 } 150 151 @Rpc(description = "Returns the name of the connected device.") bluetoothRfcommGetConnectedDeviceName( @pcParametername = "connID", description = "Connection id") @pcOptional @pcDefault"") String connID)152 public String bluetoothRfcommGetConnectedDeviceName( 153 @RpcParameter(name = "connID", description = "Connection id") 154 @RpcOptional @RpcDefault("") 155 String connID) 156 throws IOException { 157 BluetoothConnection conn = getConnection(connID); 158 return conn.getConnectedDeviceName(); 159 } 160 161 @Rpc(description = "Begins a thread to accept an Rfcomm connection over Bluetooth. ") bluetoothRfcommBeginAcceptThread( @pcParametername = "uuid") @pcDefaultDEFAULT_UUID) String uuid, @RpcParameter(name = "timeout", description = "How long to wait for a new connection, 0 is wait for ever") @RpcDefault("0") Integer timeout)162 public void bluetoothRfcommBeginAcceptThread( 163 @RpcParameter(name = "uuid") @RpcDefault(DEFAULT_UUID) String uuid, 164 @RpcParameter(name = "timeout", 165 description = "How long to wait for a new connection, 0 is wait for ever") 166 @RpcDefault("0") Integer timeout) 167 throws IOException { 168 Log.d("Accept bluetooth connection"); 169 BluetoothServerSocket mServerSocket; 170 AcceptThread acceptThread = new AcceptThread(uuid, timeout.intValue()); 171 acceptThread.start(); 172 mAcceptThread = acceptThread; 173 } 174 175 @Rpc(description = "Sends ASCII characters over the currently open Bluetooth connection.") bluetoothRfcommWrite(@pcParametername = "ascii") String ascii, @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") String connID)176 public void bluetoothRfcommWrite(@RpcParameter(name = "ascii") String ascii, 177 @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") String connID) 178 throws IOException { 179 BluetoothConnection conn = getConnection(connID); 180 try { 181 conn.write(ascii); 182 } catch (IOException e) { 183 connections.remove(conn.getUUID()); 184 throw e; 185 } 186 } 187 188 @Rpc(description = "Read up to bufferSize ASCII characters.") bluetoothRfcommRead( @pcParametername = "bufferSize") @pcDefault"4096") Integer bufferSize, @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") String connID)189 public String bluetoothRfcommRead( 190 @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize, 191 @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") 192 String connID) 193 throws IOException { 194 BluetoothConnection conn = getConnection(connID); 195 try { 196 return conn.read(bufferSize); 197 } catch (IOException e) { 198 connections.remove(conn.getUUID()); 199 throw e; 200 } 201 } 202 203 @Rpc(description = "Send bytes over the currently open Bluetooth connection.") bluetoothRfcommWriteBinary( @pcParametername = "base64", description = "A base64 encoded String of the bytes to be sent.") String base64, @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") @RpcOptional String connID)204 public void bluetoothRfcommWriteBinary( 205 @RpcParameter(name = "base64", 206 description = "A base64 encoded String of the bytes to be sent.") 207 String base64, 208 @RpcParameter(name = "connID", description = "Connection id") 209 @RpcDefault("") @RpcOptional 210 String connID) 211 throws IOException { 212 BluetoothConnection conn = getConnection(connID); 213 try { 214 conn.write(Base64Codec.decodeBase64(base64)); 215 } catch (IOException e) { 216 connections.remove(conn.getUUID()); 217 throw e; 218 } 219 } 220 221 @Rpc(description = "Read up to bufferSize bytes and return a chunked, base64 encoded string.") bluetoothRfcommReadBinary( @pcParametername = "bufferSize") @pcDefault"4096") Integer bufferSize, @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") @RpcOptional String connID)222 public String bluetoothRfcommReadBinary( 223 @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize, 224 @RpcParameter(name = "connID", description = "Connection id") 225 @RpcDefault("") @RpcOptional 226 String connID) 227 throws IOException { 228 229 BluetoothConnection conn = getConnection(connID); 230 try { 231 return Base64Codec.encodeBase64String(conn.readBinary(bufferSize)); 232 } catch (IOException e) { 233 connections.remove(conn.getUUID()); 234 throw e; 235 } 236 } 237 238 @Rpc(description = "Returns True if the next read is guaranteed not to block.") bluetoothRfcommReadReady( @pcParametername = "connID", description = "Connection id") @pcDefault"") @pcOptional String connID)239 public Boolean bluetoothRfcommReadReady( 240 @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") @RpcOptional 241 String connID) 242 throws IOException { 243 BluetoothConnection conn = getConnection(connID); 244 try { 245 return conn.readReady(); 246 } catch (IOException e) { 247 connections.remove(conn.getUUID()); 248 throw e; 249 } 250 } 251 252 @Rpc(description = "Read the next line.") bluetoothRfcommReadLine( @pcParametername = "connID", description = "Connection id") @pcOptional @pcDefault"") String connID)253 public String bluetoothRfcommReadLine( 254 @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") 255 String connID) 256 throws IOException { 257 BluetoothConnection conn = getConnection(connID); 258 try { 259 return conn.readLine(); 260 } catch (IOException e) { 261 connections.remove(conn.getUUID()); 262 throw e; 263 } 264 } 265 266 @Rpc(description = "Stops Bluetooth connection.") bluetoothRfcommStop( @pcParameter name = "connID", description = "Connection id") @pcOptional @pcDefault"") String connID)267 public void bluetoothRfcommStop( 268 @RpcParameter 269 (name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") 270 String connID) { 271 BluetoothConnection conn; 272 try { 273 conn = getConnection(connID); 274 } catch (IOException e) { 275 e.printStackTrace(); 276 return; 277 } 278 if (conn == null) { 279 return; 280 } 281 282 conn.stop(); 283 connections.remove(conn.getUUID()); 284 285 if (mAcceptThread != null) { 286 mAcceptThread.cancel(); 287 } 288 if (mConnectThread != null) { 289 mConnectThread.cancel(); 290 } 291 } 292 293 @Override shutdown()294 public void shutdown() { 295 for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) { 296 entry.getValue().stop(); 297 } 298 connections.clear(); 299 if (mAcceptThread != null) { 300 mAcceptThread.cancel(); 301 } 302 if (mConnectThread != null) { 303 mConnectThread.cancel(); 304 } 305 } 306 307 private class ConnectThread extends Thread { 308 private final BluetoothSocket mmSocket; 309 ConnectThread(BluetoothDevice device, String uuid)310 public ConnectThread(BluetoothDevice device, String uuid) { 311 BluetoothSocket tmp = null; 312 try { 313 tmp = device.createRfcommSocketToServiceRecord(UUID.fromString(uuid)); 314 } catch (IOException createSocketException) { 315 Log.e("Failed to create socket: " + createSocketException.toString()); 316 } 317 mmSocket = tmp; 318 } 319 run()320 public void run() { 321 mBluetoothAdapter.cancelDiscovery(); 322 try { 323 BluetoothConnection conn; 324 mmSocket.connect(); 325 conn = new BluetoothConnection(mmSocket); 326 Log.d("Connection Successful"); 327 addConnection(conn); 328 } catch(IOException connectException) { 329 cancel(); 330 return; 331 } 332 } 333 cancel()334 public void cancel() { 335 if (mmSocket != null) { 336 try { 337 mmSocket.close(); 338 } catch (IOException closeException){ 339 Log.e("Failed to close socket: " + closeException.toString()); 340 } 341 } 342 } 343 getSocket()344 public BluetoothSocket getSocket() { 345 return mmSocket; 346 } 347 } 348 349 350 private class AcceptThread extends Thread { 351 private final BluetoothServerSocket mmServerSocket; 352 private final int mTimeout; 353 private BluetoothSocket mmSocket; 354 AcceptThread(String uuid, int timeout)355 public AcceptThread(String uuid, int timeout) { 356 BluetoothServerSocket tmp = null; 357 mTimeout = timeout; 358 try { 359 tmp = 360 mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SDP_NAME, UUID.fromString(uuid)); 361 } catch (IOException createSocketException) { 362 Log.e("Failed to create socket: " + createSocketException.toString()); 363 } 364 mmServerSocket = tmp; 365 } 366 run()367 public void run() { 368 try { 369 mmSocket = mmServerSocket.accept(mTimeout); 370 BluetoothConnection conn = new BluetoothConnection(mmSocket, mmServerSocket); 371 addConnection(conn); 372 } catch(IOException connectException) { 373 Log.e("Failed to connect socket: " + connectException.toString()); 374 if (mmSocket != null) { 375 cancel(); 376 } 377 return; 378 } 379 } 380 cancel()381 public void cancel() { 382 if (mmSocket != null) { 383 try { 384 mmSocket.close(); 385 } catch (IOException closeException){ 386 Log.e("Failed to close socket: " + closeException.toString()); 387 } 388 } 389 if (mmServerSocket != null) { 390 try{ 391 mmServerSocket.close(); 392 } catch (IOException closeException) { 393 Log.e("Failed to close socket: " + closeException.toString()); 394 } 395 } 396 } 397 getSocket()398 public BluetoothSocket getSocket() { 399 return mmSocket; 400 } 401 } 402 403 } 404 405 406 class BluetoothConnection { 407 private BluetoothSocket mSocket; 408 private BluetoothDevice mDevice; 409 private OutputStream mOutputStream; 410 private InputStream mInputStream; 411 private BufferedReader mReader; 412 private BluetoothServerSocket mServerSocket; 413 private String UUID; 414 BluetoothConnection(BluetoothSocket mSocket)415 public BluetoothConnection(BluetoothSocket mSocket) throws IOException { 416 this(mSocket, null); 417 } 418 BluetoothConnection(BluetoothSocket mSocket, BluetoothServerSocket mServerSocket)419 public BluetoothConnection(BluetoothSocket mSocket, BluetoothServerSocket mServerSocket) 420 throws IOException { 421 this.mSocket = mSocket; 422 mOutputStream = mSocket.getOutputStream(); 423 mInputStream = mSocket.getInputStream(); 424 mDevice = mSocket.getRemoteDevice(); 425 mReader = new BufferedReader(new InputStreamReader(mInputStream, "ASCII")); 426 this.mServerSocket = mServerSocket; 427 } 428 setUUID(String UUID)429 public void setUUID(String UUID) { 430 this.UUID = UUID; 431 } 432 getUUID()433 public String getUUID() { 434 return UUID; 435 } 436 getRemoteBluetoothAddress()437 public String getRemoteBluetoothAddress() { 438 return mDevice.getAddress(); 439 } 440 isConnected()441 public boolean isConnected() { 442 if (mSocket == null) { 443 return false; 444 } 445 try { 446 mSocket.getRemoteDevice(); 447 mInputStream.available(); 448 mReader.ready(); 449 return true; 450 } catch (Exception e) { 451 return false; 452 } 453 } 454 write(byte[] out)455 public void write(byte[] out) throws IOException { 456 if (mOutputStream != null) { 457 mOutputStream.write(out); 458 } else { 459 throw new IOException("Bluetooth not ready."); 460 } 461 } 462 write(String out)463 public void write(String out) throws IOException { 464 this.write(out.getBytes()); 465 } 466 readReady()467 public Boolean readReady() throws IOException { 468 if (mReader != null) { 469 return mReader.ready(); 470 } 471 throw new IOException("Bluetooth not ready."); 472 } 473 readBinary()474 public byte[] readBinary() throws IOException { 475 return this.readBinary(4096); 476 } 477 readBinary(int bufferSize)478 public byte[] readBinary(int bufferSize) throws IOException { 479 if (mReader != null) { 480 byte[] buffer = new byte[bufferSize]; 481 int bytesRead = mInputStream.read(buffer); 482 if (bytesRead == -1) { 483 Log.e("Read failed."); 484 throw new IOException("Read failed."); 485 } 486 byte[] truncatedBuffer = new byte[bytesRead]; 487 System.arraycopy(buffer, 0, truncatedBuffer, 0, bytesRead); 488 return truncatedBuffer; 489 } 490 491 throw new IOException("Bluetooth not ready."); 492 493 } 494 read()495 public String read() throws IOException { 496 return this.read(4096); 497 } 498 read(int bufferSize)499 public String read(int bufferSize) throws IOException { 500 if (mReader != null) { 501 char[] buffer = new char[bufferSize]; 502 int bytesRead = mReader.read(buffer); 503 if (bytesRead == -1) { 504 Log.e("Read failed."); 505 throw new IOException("Read failed."); 506 } 507 return new String(buffer, 0, bytesRead); 508 } 509 throw new IOException("Bluetooth not ready."); 510 } 511 readLine()512 public String readLine() throws IOException { 513 if (mReader != null) { 514 return mReader.readLine(); 515 } 516 throw new IOException("Bluetooth not ready."); 517 } 518 getConnectedDeviceName()519 public String getConnectedDeviceName() { 520 return mDevice.getName(); 521 } 522 clearFileDescriptor()523 private synchronized void clearFileDescriptor() { 524 try { 525 Field field = BluetoothSocket.class.getDeclaredField("mPfd"); 526 field.setAccessible(true); 527 ParcelFileDescriptor mPfd = (ParcelFileDescriptor) field.get(mSocket); 528 Log.d("Closing mPfd: " + mPfd); 529 if (mPfd == null) 530 return; 531 mPfd.close(); 532 mPfd = null; 533 try { field.set(mSocket, mPfd); } 534 catch(Exception e) { 535 Log.d("Exception setting mPfd = null in cleanCloseFix(): " + e.toString()); 536 } 537 } catch (Exception e) { 538 Log.w("ParcelFileDescriptor could not be cleanly closed.", e); 539 } 540 } 541 stop()542 public void stop() { 543 if (mSocket != null) { 544 try { 545 mSocket.close(); 546 clearFileDescriptor(); 547 } catch (IOException e) { 548 Log.e(e); 549 } 550 } 551 mSocket = null; 552 if (mServerSocket != null) { 553 try { 554 mServerSocket.close(); 555 } catch (IOException e) { 556 Log.e(e); 557 } 558 } 559 mServerSocket = null; 560 561 if (mInputStream != null) { 562 try { 563 mInputStream.close(); 564 } catch (IOException e) { 565 Log.e(e); 566 } 567 } 568 mInputStream = null; 569 if (mOutputStream != null) { 570 try { 571 mOutputStream.close(); 572 } catch (IOException e) { 573 Log.e(e); 574 } 575 } 576 mOutputStream = null; 577 if (mReader != null) { 578 try { 579 mReader.close(); 580 } catch (IOException e) { 581 Log.e(e); 582 } 583 } 584 mReader = null; 585 } 586 } 587