Home | History | Annotate | Download | only in bluetooth
      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 
     69   public BluetoothRfcommFacade(FacadeManager manager) {
     70     super(manager);
     71     mEventFacade = manager.getReceiver(EventFacade.class);
     72     mService = manager.getService();
     73     mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
     74   }
     75 
     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 
     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. ")
     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")
    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")
    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")
    135   public void bluetoothRfcommEndAcceptThread()
    136     throws IOException {
    137     mAcceptThread.cancel();
    138   }
    139 
    140   @Rpc(description = "Returns active Bluetooth connections.")
    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.")
    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. ")
    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.")
    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.")
    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.")
    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.")
    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.")
    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.")
    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.")
    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
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    415   public BluetoothConnection(BluetoothSocket mSocket) throws IOException {
    416     this(mSocket, null);
    417   }
    418 
    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 
    429   public void setUUID(String UUID) {
    430     this.UUID = UUID;
    431   }
    432 
    433   public String getUUID() {
    434     return UUID;
    435   }
    436 
    437   public String getRemoteBluetoothAddress() {
    438     return mDevice.getAddress();
    439   }
    440 
    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 
    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 
    463   public void write(String out) throws IOException {
    464     this.write(out.getBytes());
    465   }
    466 
    467   public Boolean readReady() throws IOException {
    468     if (mReader != null) {
    469       return mReader.ready();
    470     }
    471     throw new IOException("Bluetooth not ready.");
    472   }
    473 
    474   public byte[] readBinary() throws IOException {
    475     return this.readBinary(4096);
    476   }
    477 
    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 
    495   public String read() throws IOException {
    496     return this.read(4096);
    497   }
    498 
    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 
    512   public String readLine() throws IOException {
    513     if (mReader != null) {
    514       return mReader.readLine();
    515     }
    516     throw new IOException("Bluetooth not ready.");
    517   }
    518 
    519   public String getConnectedDeviceName() {
    520     return mDevice.getName();
    521   }
    522 
    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 
    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