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.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 46 import org.apache.commons.codec.binary.Base64Codec; 47 48 /** 49 * Bluetooth functions. 50 * 51 */ 52 // Discovery functions added by Eden Sayag 53 54 public class BluetoothRfcommFacade extends RpcReceiver { 55 56 // UUID for SL4A. 57 private static final String DEFAULT_UUID = "457807c0-4897-11df-9879-0800200c9a66"; 58 private static final String SDP_NAME = "SL4A"; 59 private final Service mService; 60 private final BluetoothPairingHelper mPairingReceiver; 61 private final BluetoothAdapter mBluetoothAdapter; 62 private Map<String, BluetoothConnection> 63 connections = new HashMap<String, BluetoothConnection>(); 64 private BluetoothSocket mCurrentSocket; 65 private ConnectThread mCurrThread; 66 BluetoothRfcommFacade(FacadeManager manager)67 public BluetoothRfcommFacade(FacadeManager manager) { 68 super(manager); 69 mService = manager.getService(); 70 mPairingReceiver = new BluetoothPairingHelper(); 71 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 72 } 73 getConnection(String connID)74 private BluetoothConnection getConnection(String connID) throws IOException { 75 BluetoothConnection conn = null; 76 if (connID.trim().length() > 0) { 77 conn = connections.get(connID); 78 } else if (connections.size() == 1) { 79 conn = (BluetoothConnection) connections.values().toArray()[0]; 80 } 81 if (conn == null) { 82 throw new IOException("Bluetooth not ready for this connID."); 83 } 84 return conn; 85 } 86 addConnection(BluetoothConnection conn)87 private String addConnection(BluetoothConnection conn) { 88 String uuid = UUID.randomUUID().toString(); 89 connections.put(uuid, conn); 90 conn.setUUID(uuid); 91 return uuid; 92 } 93 94 @Rpc(description = "Connect to a device over Bluetooth. " 95 + "Blocks until the connection is established or fails.", 96 returns = "True if the connection was established successfully.") bluetoothRfcommConnect( @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 String bluetoothRfcommConnect( 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 BluetoothSocket mSocket; 107 BluetoothConnection conn; 108 mDevice = mBluetoothAdapter.getRemoteDevice(address); 109 // Register a broadcast receiver to bypass manual confirmation 110 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 111 mService.registerReceiver(mPairingReceiver, filter); 112 ConnectThread t = new ConnectThread(mDevice, uuid); 113 t.run(); 114 mCurrThread = t; 115 conn = new BluetoothConnection(mCurrThread.getSocket()); 116 Log.d("Connection Successful"); 117 mService.unregisterReceiver(mPairingReceiver); 118 return addConnection(conn); 119 } 120 121 @Rpc(description = "Kill thread") bluetoothRfcommKillConnThread()122 public void bluetoothRfcommKillConnThread() { 123 try { 124 mCurrThread.cancel(); 125 mCurrThread.join(5000); 126 } catch (InterruptedException e) { 127 Log.e("Interrupted Exception: " + e.toString()); 128 } 129 } 130 131 /** 132 * Closes an active Rfcomm socket 133 */ 134 @Rpc(description = "Close an active Rfcomm socket") bluetoothRfcommCloseSocket()135 public void bluetoothRfcommCloseSocket() 136 throws IOException { 137 mCurrentSocket.close(); 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 = "Listens for and accepts a Bluetooth connection." 162 + "Blocks until the connection is established or fails.") bluetoothRfcommAccept( @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)163 public String bluetoothRfcommAccept( 164 @RpcParameter(name = "uuid") @RpcDefault(DEFAULT_UUID) String uuid, 165 @RpcParameter(name = "timeout", 166 description = "How long to wait for a new connection, 0 is wait for ever") 167 @RpcDefault("0") Integer timeout) 168 throws IOException { 169 Log.d("Accept bluetooth connection"); 170 BluetoothServerSocket mServerSocket; 171 mServerSocket = 172 mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SDP_NAME, UUID.fromString(uuid)); 173 // Register a broadcast receiver to bypass manual confirmation 174 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); 175 mService.registerReceiver(mPairingReceiver, filter); 176 177 BluetoothSocket mSocket = mServerSocket.accept(timeout.intValue()); 178 BluetoothConnection conn = new BluetoothConnection(mSocket, mServerSocket); 179 mService.unregisterReceiver(mPairingReceiver); 180 mCurrentSocket = mSocket; 181 return addConnection(conn); 182 } 183 184 @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)185 public void bluetoothRfcommWrite(@RpcParameter(name = "ascii") String ascii, 186 @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") String connID) 187 throws IOException { 188 BluetoothConnection conn = getConnection(connID); 189 try { 190 conn.write(ascii); 191 } catch (IOException e) { 192 connections.remove(conn.getUUID()); 193 throw e; 194 } 195 } 196 197 @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)198 public String bluetoothRfcommRead( 199 @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize, 200 @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") 201 String connID) 202 throws IOException { 203 BluetoothConnection conn = getConnection(connID); 204 try { 205 return conn.read(bufferSize); 206 } catch (IOException e) { 207 connections.remove(conn.getUUID()); 208 throw e; 209 } 210 } 211 212 @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)213 public void bluetoothRfcommWriteBinary( 214 @RpcParameter(name = "base64", 215 description = "A base64 encoded String of the bytes to be sent.") 216 String base64, 217 @RpcParameter(name = "connID", description = "Connection id") 218 @RpcDefault("") @RpcOptional 219 String connID) 220 throws IOException { 221 BluetoothConnection conn = getConnection(connID); 222 try { 223 conn.write(Base64Codec.decodeBase64(base64)); 224 } catch (IOException e) { 225 connections.remove(conn.getUUID()); 226 throw e; 227 } 228 } 229 230 @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)231 public String bluetoothRfcommReadBinary( 232 @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize, 233 @RpcParameter(name = "connID", description = "Connection id") 234 @RpcDefault("") @RpcOptional 235 String connID) 236 throws IOException { 237 238 BluetoothConnection conn = getConnection(connID); 239 try { 240 return Base64Codec.encodeBase64String(conn.readBinary(bufferSize)); 241 } catch (IOException e) { 242 connections.remove(conn.getUUID()); 243 throw e; 244 } 245 } 246 247 @Rpc(description = "Returns True if the next read is guaranteed not to block.") bluetoothRfcommReadReady( @pcParametername = "connID", description = "Connection id") @pcDefault"") @pcOptional String connID)248 public Boolean bluetoothRfcommReadReady( 249 @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") @RpcOptional 250 String connID) 251 throws IOException { 252 BluetoothConnection conn = getConnection(connID); 253 try { 254 return conn.readReady(); 255 } catch (IOException e) { 256 connections.remove(conn.getUUID()); 257 throw e; 258 } 259 } 260 261 @Rpc(description = "Read the next line.") bluetoothRfcommReadLine( @pcParametername = "connID", description = "Connection id") @pcOptional @pcDefault"") String connID)262 public String bluetoothRfcommReadLine( 263 @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") 264 String connID) 265 throws IOException { 266 BluetoothConnection conn = getConnection(connID); 267 try { 268 return conn.readLine(); 269 } catch (IOException e) { 270 connections.remove(conn.getUUID()); 271 throw e; 272 } 273 } 274 275 @Rpc(description = "Stops Bluetooth connection.") bluetoothRfcommStop( @pcParameter name = "connID", description = "Connection id") @pcOptional @pcDefault"") String connID)276 public void bluetoothRfcommStop( 277 @RpcParameter 278 (name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") 279 String connID) { 280 BluetoothConnection conn; 281 try { 282 conn = getConnection(connID); 283 } catch (IOException e) { 284 e.printStackTrace(); 285 return; 286 } 287 if (conn == null) { 288 return; 289 } 290 291 conn.stop(); 292 connections.remove(conn.getUUID()); 293 } 294 295 @Override shutdown()296 public void shutdown() { 297 for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) { 298 entry.getValue().stop(); 299 } 300 connections.clear(); 301 } 302 private class ConnectThread extends Thread { 303 private final BluetoothSocket mmSocket; 304 ConnectThread(BluetoothDevice device, String uuid)305 public ConnectThread(BluetoothDevice device, String uuid) { 306 BluetoothSocket tmp = null; 307 try { 308 tmp = device.createRfcommSocketToServiceRecord(UUID.fromString(uuid)); 309 } catch (IOException createSocketException) { 310 Log.e("Failed to create socket: " + createSocketException.toString()); 311 } 312 mmSocket = tmp; 313 } 314 run()315 public void run() { 316 mBluetoothAdapter.cancelDiscovery(); 317 try { 318 mmSocket.connect(); 319 } catch(IOException connectException) { 320 Log.e("Failed to connect socket: " + connectException.toString()); 321 try { 322 mmSocket.close(); 323 } catch(IOException closeException){ 324 Log.e("Failed to close socket: " + closeException.toString()); 325 } 326 return; 327 } 328 } 329 cancel()330 public void cancel() { 331 try { 332 mmSocket.close(); 333 } catch (IOException e){ 334 335 } 336 } 337 getSocket()338 public BluetoothSocket getSocket() { 339 return mmSocket; 340 } 341 } 342 343 } 344 345 346 class BluetoothConnection { 347 private BluetoothSocket mSocket; 348 private BluetoothDevice mDevice; 349 private OutputStream mOutputStream; 350 private InputStream mInputStream; 351 private BufferedReader mReader; 352 private BluetoothServerSocket mServerSocket; 353 private String UUID; 354 BluetoothConnection(BluetoothSocket mSocket)355 public BluetoothConnection(BluetoothSocket mSocket) throws IOException { 356 this(mSocket, null); 357 } 358 BluetoothConnection(BluetoothSocket mSocket, BluetoothServerSocket mServerSocket)359 public BluetoothConnection(BluetoothSocket mSocket, BluetoothServerSocket mServerSocket) 360 throws IOException { 361 this.mSocket = mSocket; 362 mOutputStream = mSocket.getOutputStream(); 363 mInputStream = mSocket.getInputStream(); 364 mDevice = mSocket.getRemoteDevice(); 365 mReader = new BufferedReader(new InputStreamReader(mInputStream, "ASCII")); 366 this.mServerSocket = mServerSocket; 367 } 368 setUUID(String UUID)369 public void setUUID(String UUID) { 370 this.UUID = UUID; 371 } 372 getUUID()373 public String getUUID() { 374 return UUID; 375 } 376 getRemoteBluetoothAddress()377 public String getRemoteBluetoothAddress() { 378 return mDevice.getAddress(); 379 } 380 isConnected()381 public boolean isConnected() { 382 if (mSocket == null) { 383 return false; 384 } 385 try { 386 mSocket.getRemoteDevice(); 387 mInputStream.available(); 388 mReader.ready(); 389 return true; 390 } catch (Exception e) { 391 return false; 392 } 393 } 394 write(byte[] out)395 public void write(byte[] out) throws IOException { 396 if (mOutputStream != null) { 397 mOutputStream.write(out); 398 } else { 399 throw new IOException("Bluetooth not ready."); 400 } 401 } 402 write(String out)403 public void write(String out) throws IOException { 404 this.write(out.getBytes()); 405 } 406 readReady()407 public Boolean readReady() throws IOException { 408 if (mReader != null) { 409 return mReader.ready(); 410 } 411 throw new IOException("Bluetooth not ready."); 412 } 413 readBinary()414 public byte[] readBinary() throws IOException { 415 return this.readBinary(4096); 416 } 417 readBinary(int bufferSize)418 public byte[] readBinary(int bufferSize) throws IOException { 419 if (mReader != null) { 420 byte[] buffer = new byte[bufferSize]; 421 int bytesRead = mInputStream.read(buffer); 422 if (bytesRead == -1) { 423 Log.e("Read failed."); 424 throw new IOException("Read failed."); 425 } 426 byte[] truncatedBuffer = new byte[bytesRead]; 427 System.arraycopy(buffer, 0, truncatedBuffer, 0, bytesRead); 428 return truncatedBuffer; 429 } 430 431 throw new IOException("Bluetooth not ready."); 432 433 } 434 read()435 public String read() throws IOException { 436 return this.read(4096); 437 } 438 read(int bufferSize)439 public String read(int bufferSize) throws IOException { 440 if (mReader != null) { 441 char[] buffer = new char[bufferSize]; 442 int bytesRead = mReader.read(buffer); 443 if (bytesRead == -1) { 444 Log.e("Read failed."); 445 throw new IOException("Read failed."); 446 } 447 return new String(buffer, 0, bytesRead); 448 } 449 throw new IOException("Bluetooth not ready."); 450 } 451 readLine()452 public String readLine() throws IOException { 453 if (mReader != null) { 454 return mReader.readLine(); 455 } 456 throw new IOException("Bluetooth not ready."); 457 } 458 getConnectedDeviceName()459 public String getConnectedDeviceName() { 460 return mDevice.getName(); 461 } 462 clearFileDescriptor()463 private synchronized void clearFileDescriptor() { 464 try { 465 Field field = BluetoothSocket.class.getDeclaredField("mPfd"); 466 field.setAccessible(true); 467 ParcelFileDescriptor mPfd = (ParcelFileDescriptor) field.get(mSocket); 468 Log.d("Closing mPfd: " + mPfd); 469 if (mPfd == null) 470 return; 471 mPfd.close(); 472 mPfd = null; 473 try { field.set(mSocket, mPfd); } 474 catch(Exception e) { 475 Log.d("Exception setting mPfd = null in cleanCloseFix(): " + e.toString()); 476 } 477 } catch (Exception e) { 478 Log.w("ParcelFileDescriptor could not be cleanly closed.", e); 479 } 480 } 481 stop()482 public void stop() { 483 if (mSocket != null) { 484 try { 485 mSocket.close(); 486 clearFileDescriptor(); 487 } catch (IOException e) { 488 Log.e(e); 489 } 490 } 491 mSocket = null; 492 if (mServerSocket != null) { 493 try { 494 mServerSocket.close(); 495 } catch (IOException e) { 496 Log.e(e); 497 } 498 } 499 mServerSocket = null; 500 501 if (mInputStream != null) { 502 try { 503 mInputStream.close(); 504 } catch (IOException e) { 505 Log.e(e); 506 } 507 } 508 mInputStream = null; 509 if (mOutputStream != null) { 510 try { 511 mOutputStream.close(); 512 } catch (IOException e) { 513 Log.e(e); 514 } 515 } 516 mOutputStream = null; 517 if (mReader != null) { 518 try { 519 mReader.close(); 520 } catch (IOException e) { 521 Log.e(e); 522 } 523 } 524 mReader = null; 525 } 526 } 527