| /* |
| * Copyright (C) 2016 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| |
| package com.googlecode.android_scripting.facade.bluetooth; |
| |
| import android.app.Service; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothServerSocket; |
| import android.bluetooth.BluetoothSocket; |
| import android.content.IntentFilter; |
| import android.os.ParcelFileDescriptor; |
| |
| import com.googlecode.android_scripting.Log; |
| import com.googlecode.android_scripting.facade.FacadeManager; |
| import com.googlecode.android_scripting.jsonrpc.RpcReceiver; |
| import com.googlecode.android_scripting.rpc.Rpc; |
| import com.googlecode.android_scripting.rpc.RpcDefault; |
| import com.googlecode.android_scripting.rpc.RpcOptional; |
| import com.googlecode.android_scripting.rpc.RpcParameter; |
| |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.OutputStream; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.UUID; |
| import java.lang.reflect.Field; |
| |
| import org.apache.commons.codec.binary.Base64Codec; |
| |
| /** |
| * Bluetooth functions. |
| * |
| */ |
| // Discovery functions added by Eden Sayag |
| |
| public class BluetoothRfcommFacade extends RpcReceiver { |
| |
| // UUID for SL4A. |
| private static final String DEFAULT_UUID = "457807c0-4897-11df-9879-0800200c9a66"; |
| private static final String SDP_NAME = "SL4A"; |
| private final Service mService; |
| private final BluetoothPairingHelper mPairingReceiver; |
| private final BluetoothAdapter mBluetoothAdapter; |
| private Map<String, BluetoothConnection> |
| connections = new HashMap<String, BluetoothConnection>(); |
| |
| public BluetoothRfcommFacade(FacadeManager manager) { |
| super(manager); |
| mService = manager.getService(); |
| mPairingReceiver = new BluetoothPairingHelper(); |
| mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| } |
| |
| private BluetoothConnection getConnection(String connID) throws IOException { |
| BluetoothConnection conn = null; |
| if (connID.trim().length() > 0) { |
| conn = connections.get(connID); |
| } else if (connections.size() == 1) { |
| conn = (BluetoothConnection) connections.values().toArray()[0]; |
| } |
| if (conn == null) { |
| throw new IOException("Bluetooth not ready for this connID."); |
| } |
| return conn; |
| } |
| |
| private String addConnection(BluetoothConnection conn) { |
| String uuid = UUID.randomUUID().toString(); |
| connections.put(uuid, conn); |
| conn.setUUID(uuid); |
| return uuid; |
| } |
| |
| @Rpc(description = "Connect to a device over Bluetooth. " |
| + "Blocks until the connection is established or fails.", |
| returns = "True if the connection was established successfully.") |
| public String bluetoothRfcommConnect( |
| @RpcParameter(name = "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) |
| throws IOException { |
| BluetoothDevice mDevice; |
| BluetoothSocket mSocket; |
| BluetoothConnection conn; |
| mDevice = mBluetoothAdapter.getRemoteDevice(address); |
| mSocket = mDevice.createRfcommSocketToServiceRecord(UUID.fromString(uuid)); |
| |
| // Register a broadcast receiver to bypass manual confirmation |
| IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); |
| mService.registerReceiver(mPairingReceiver, filter); |
| |
| // Always cancel discovery because it will slow down a connection. |
| mBluetoothAdapter.cancelDiscovery(); |
| mSocket.connect(); |
| conn = new BluetoothConnection(mSocket); |
| |
| mService.unregisterReceiver(mPairingReceiver); |
| return addConnection(conn); |
| } |
| |
| @Rpc(description = "Returns active Bluetooth connections.") |
| public Map<String, String> bluetoothRfcommActiveConnections() { |
| Map<String, String> out = new HashMap<String, String>(); |
| for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) { |
| if (entry.getValue().isConnected()) { |
| out.put(entry.getKey(), entry.getValue().getRemoteBluetoothAddress()); |
| } |
| } |
| return out; |
| } |
| |
| @Rpc(description = "Returns the name of the connected device.") |
| public String bluetoothRfcommGetConnectedDeviceName( |
| @RpcParameter(name = "connID", description = "Connection id") |
| @RpcOptional @RpcDefault("") |
| String connID) |
| throws IOException { |
| BluetoothConnection conn = getConnection(connID); |
| return conn.getConnectedDeviceName(); |
| } |
| |
| @Rpc(description = "Listens for and accepts a Bluetooth connection." |
| + "Blocks until the connection is established or fails.") |
| public String bluetoothRfcommAccept( |
| @RpcParameter(name = "uuid") @RpcDefault(DEFAULT_UUID) String uuid, |
| @RpcParameter(name = "timeout", |
| description = "How long to wait for a new connection, 0 is wait for ever") |
| @RpcDefault("0") Integer timeout) |
| throws IOException { |
| Log.d("Accept bluetooth connection"); |
| BluetoothServerSocket mServerSocket; |
| mServerSocket = |
| mBluetoothAdapter.listenUsingRfcommWithServiceRecord(SDP_NAME, UUID.fromString(uuid)); |
| // Register a broadcast receiver to bypass manual confirmation |
| IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); |
| mService.registerReceiver(mPairingReceiver, filter); |
| |
| BluetoothSocket mSocket = mServerSocket.accept(timeout.intValue()); |
| BluetoothConnection conn = new BluetoothConnection(mSocket, mServerSocket); |
| mService.unregisterReceiver(mPairingReceiver); |
| return addConnection(conn); |
| } |
| |
| @Rpc(description = "Sends ASCII characters over the currently open Bluetooth connection.") |
| public void bluetoothRfcommWrite(@RpcParameter(name = "ascii") String ascii, |
| @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") String connID) |
| throws IOException { |
| BluetoothConnection conn = getConnection(connID); |
| try { |
| conn.write(ascii); |
| } catch (IOException e) { |
| connections.remove(conn.getUUID()); |
| throw e; |
| } |
| } |
| |
| @Rpc(description = "Read up to bufferSize ASCII characters.") |
| public String bluetoothRfcommRead( |
| @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize, |
| @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") |
| String connID) |
| throws IOException { |
| BluetoothConnection conn = getConnection(connID); |
| try { |
| return conn.read(bufferSize); |
| } catch (IOException e) { |
| connections.remove(conn.getUUID()); |
| throw e; |
| } |
| } |
| |
| @Rpc(description = "Send bytes over the currently open Bluetooth connection.") |
| public void bluetoothRfcommWriteBinary( |
| @RpcParameter(name = "base64", |
| description = "A base64 encoded String of the bytes to be sent.") |
| String base64, |
| @RpcParameter(name = "connID", description = "Connection id") |
| @RpcDefault("") @RpcOptional |
| String connID) |
| throws IOException { |
| BluetoothConnection conn = getConnection(connID); |
| try { |
| conn.write(Base64Codec.decodeBase64(base64)); |
| } catch (IOException e) { |
| connections.remove(conn.getUUID()); |
| throw e; |
| } |
| } |
| |
| @Rpc(description = "Read up to bufferSize bytes and return a chunked, base64 encoded string.") |
| public String bluetoothRfcommReadBinary( |
| @RpcParameter(name = "bufferSize") @RpcDefault("4096") Integer bufferSize, |
| @RpcParameter(name = "connID", description = "Connection id") |
| @RpcDefault("") @RpcOptional |
| String connID) |
| throws IOException { |
| |
| BluetoothConnection conn = getConnection(connID); |
| try { |
| return Base64Codec.encodeBase64String(conn.readBinary(bufferSize)); |
| } catch (IOException e) { |
| connections.remove(conn.getUUID()); |
| throw e; |
| } |
| } |
| |
| @Rpc(description = "Returns True if the next read is guaranteed not to block.") |
| public Boolean bluetoothRfcommReadReady( |
| @RpcParameter(name = "connID", description = "Connection id") @RpcDefault("") @RpcOptional |
| String connID) |
| throws IOException { |
| BluetoothConnection conn = getConnection(connID); |
| try { |
| return conn.readReady(); |
| } catch (IOException e) { |
| connections.remove(conn.getUUID()); |
| throw e; |
| } |
| } |
| |
| @Rpc(description = "Read the next line.") |
| public String bluetoothRfcommReadLine( |
| @RpcParameter(name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") |
| String connID) |
| throws IOException { |
| BluetoothConnection conn = getConnection(connID); |
| try { |
| return conn.readLine(); |
| } catch (IOException e) { |
| connections.remove(conn.getUUID()); |
| throw e; |
| } |
| } |
| |
| @Rpc(description = "Stops Bluetooth connection.") |
| public void bluetoothRfcommStop( |
| @RpcParameter |
| (name = "connID", description = "Connection id") @RpcOptional @RpcDefault("") |
| String connID) { |
| BluetoothConnection conn; |
| try { |
| conn = getConnection(connID); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return; |
| } |
| if (conn == null) { |
| return; |
| } |
| |
| conn.stop(); |
| connections.remove(conn.getUUID()); |
| } |
| |
| @Override |
| public void shutdown() { |
| for (Map.Entry<String, BluetoothConnection> entry : connections.entrySet()) { |
| entry.getValue().stop(); |
| } |
| connections.clear(); |
| } |
| } |
| |
| class BluetoothConnection { |
| private BluetoothSocket mSocket; |
| private BluetoothDevice mDevice; |
| private OutputStream mOutputStream; |
| private InputStream mInputStream; |
| private BufferedReader mReader; |
| private BluetoothServerSocket mServerSocket; |
| private String UUID; |
| |
| public BluetoothConnection(BluetoothSocket mSocket) throws IOException { |
| this(mSocket, null); |
| } |
| |
| public BluetoothConnection(BluetoothSocket mSocket, BluetoothServerSocket mServerSocket) |
| throws IOException { |
| this.mSocket = mSocket; |
| mOutputStream = mSocket.getOutputStream(); |
| mInputStream = mSocket.getInputStream(); |
| mDevice = mSocket.getRemoteDevice(); |
| mReader = new BufferedReader(new InputStreamReader(mInputStream, "ASCII")); |
| this.mServerSocket = mServerSocket; |
| } |
| |
| public void setUUID(String UUID) { |
| this.UUID = UUID; |
| } |
| |
| public String getUUID() { |
| return UUID; |
| } |
| |
| public String getRemoteBluetoothAddress() { |
| return mDevice.getAddress(); |
| } |
| |
| public boolean isConnected() { |
| if (mSocket == null) { |
| return false; |
| } |
| try { |
| mSocket.getRemoteDevice(); |
| mInputStream.available(); |
| mReader.ready(); |
| return true; |
| } catch (Exception e) { |
| return false; |
| } |
| } |
| |
| public void write(byte[] out) throws IOException { |
| if (mOutputStream != null) { |
| mOutputStream.write(out); |
| } else { |
| throw new IOException("Bluetooth not ready."); |
| } |
| } |
| |
| public void write(String out) throws IOException { |
| this.write(out.getBytes()); |
| } |
| |
| public Boolean readReady() throws IOException { |
| if (mReader != null) { |
| return mReader.ready(); |
| } |
| throw new IOException("Bluetooth not ready."); |
| } |
| |
| public byte[] readBinary() throws IOException { |
| return this.readBinary(4096); |
| } |
| |
| public byte[] readBinary(int bufferSize) throws IOException { |
| if (mReader != null) { |
| byte[] buffer = new byte[bufferSize]; |
| int bytesRead = mInputStream.read(buffer); |
| if (bytesRead == -1) { |
| Log.e("Read failed."); |
| throw new IOException("Read failed."); |
| } |
| byte[] truncatedBuffer = new byte[bytesRead]; |
| System.arraycopy(buffer, 0, truncatedBuffer, 0, bytesRead); |
| return truncatedBuffer; |
| } |
| |
| throw new IOException("Bluetooth not ready."); |
| |
| } |
| |
| public String read() throws IOException { |
| return this.read(4096); |
| } |
| |
| public String read(int bufferSize) throws IOException { |
| if (mReader != null) { |
| char[] buffer = new char[bufferSize]; |
| int bytesRead = mReader.read(buffer); |
| if (bytesRead == -1) { |
| Log.e("Read failed."); |
| throw new IOException("Read failed."); |
| } |
| return new String(buffer, 0, bytesRead); |
| } |
| throw new IOException("Bluetooth not ready."); |
| } |
| |
| public String readLine() throws IOException { |
| if (mReader != null) { |
| return mReader.readLine(); |
| } |
| throw new IOException("Bluetooth not ready."); |
| } |
| |
| public String getConnectedDeviceName() { |
| return mDevice.getName(); |
| } |
| |
| private synchronized void clearFileDescriptor() { |
| try { |
| Field field = BluetoothSocket.class.getDeclaredField("mPfd"); |
| field.setAccessible(true); |
| ParcelFileDescriptor mPfd = (ParcelFileDescriptor) field.get(mSocket); |
| if (mPfd == null) |
| return; |
| mPfd.close(); |
| mPfd = null; |
| try { field.set(mSocket, mPfd); } |
| catch(Exception e) { |
| Log.d("Exception setting mPfd = null in cleanCloseFix(): " + e.toString()); |
| } |
| } catch (Exception e) { |
| Log.w("ParcelFileDescriptor could not be cleanly closed.", e); |
| } |
| } |
| |
| public void stop() { |
| if (mSocket != null) { |
| try { |
| clearFileDescriptor(); |
| mSocket.close(); |
| } catch (IOException e) { |
| Log.e(e); |
| } |
| } |
| mSocket = null; |
| if (mServerSocket != null) { |
| try { |
| mServerSocket.close(); |
| } catch (IOException e) { |
| Log.e(e); |
| } |
| } |
| mServerSocket = null; |
| |
| if (mInputStream != null) { |
| try { |
| mInputStream.close(); |
| } catch (IOException e) { |
| Log.e(e); |
| } |
| } |
| mInputStream = null; |
| if (mOutputStream != null) { |
| try { |
| mOutputStream.close(); |
| } catch (IOException e) { |
| Log.e(e); |
| } |
| } |
| mOutputStream = null; |
| if (mReader != null) { |
| try { |
| mReader.close(); |
| } catch (IOException e) { |
| Log.e(e); |
| } |
| } |
| mReader = null; |
| } |
| } |