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