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