| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package io.v.android.impl.google.rpc.protocols.bt; |
| |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothServerSocket; |
| import android.bluetooth.BluetoothSocket; |
| import android.util.Log; |
| |
| import com.google.common.base.Splitter; |
| |
| import org.joda.time.Duration; |
| |
| import java.io.InputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.lang.reflect.Method; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| |
| import io.v.android.v23.V; |
| import io.v.v23.context.VContext; |
| import io.v.v23.verror.VException; |
| |
| /** |
| * Handles bluetooth connection establishment on Android. |
| * <p> |
| * Used as a helper class for native code which sets up and registers the bluetooth protocol with |
| * the vanadium RPC service. |
| */ |
| class BluetoothWithPort { |
| private static final String TAG = "Bluetooth"; |
| |
| static Listener listen(VContext ctx, String btAddr) throws Exception { |
| String macAddr = getMACAddress(ctx, btAddr); |
| int port = getPortNumber(btAddr); |
| BluetoothServerSocket socket = listenOnPort(port); |
| if (port == 0) { |
| // listen on the first available port. Get the port number. |
| port = getPortNumber(socket); |
| } |
| Log.d(TAG, String.format("listening on port %d", port)); |
| return new Listener(socket, String.format("%s/%d", macAddr, port)); |
| } |
| |
| static Stream dial(VContext ctx, String btAddr, Duration timeout) throws Exception { |
| String macAddr = getMACAddress(ctx, btAddr); |
| int port = getPortNumber(btAddr); |
| BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddr); |
| |
| // Create a socket to the remote device. |
| // NOTE(spetrovic): Android's public methods currently only allow connection to |
| // a UUID, which goes through SDP. Since we already have a remote port number, |
| // we connect to it directly, invoking a hidden method using reflection. |
| Method m = |
| device.getClass().getMethod("createInsecureRfcommSocket", new Class[] {int.class}); |
| final BluetoothSocket socket = (BluetoothSocket) m.invoke(device, port); |
| // Connect. |
| Timer timer = null; |
| if (timeout.getMillis() != 0) { |
| timer = new Timer(); |
| timer.schedule( |
| new TimerTask() { |
| @Override |
| public void run() { |
| try { |
| socket.close(); |
| } catch (IOException e) { |
| } |
| } |
| }, |
| timeout.getMillis()); |
| } |
| try { |
| socket.connect(); |
| } catch (IOException e) { |
| socket.close(); |
| throw e; |
| } finally { |
| if (timer != null) { |
| timer.cancel(); |
| } |
| } |
| // There is no way currently to retrieve the local port number for the |
| // connection, but that's probably OK. |
| String localAddr = String.format("%s/%d", localMACAddress(ctx), 0); |
| String remoteAddr = String.format("%s/%d", macAddr, port); |
| return new Stream(socket, localAddr, remoteAddr); |
| } |
| |
| private static BluetoothServerSocket listenOnPort(int port) throws Exception { |
| // Note that Android developer guide says that unlike TCP/IP, RFCOMM only allows |
| // one connected client per channel at a time: |
| // https://developer.android.com/guide/topics/connectivity/bluetooth.html. |
| // But this seems to be conflict with the android reference page. |
| // https://developer.android.com/reference/android/bluetooth/BluetoothServerSocket.html#accept() |
| // |
| // Multiple client connection on a same listening channel seem to work with some testing devices |
| // like Nexus 6 or Nexus 9, but this is not guaranteed to work with other devices. |
| if (port == 0) { |
| // Use SOCKET_CHANNEL_AUTO_STATIC (-2) to auto assign a channel number. |
| port = -2; |
| } |
| // Use reflection to reach the hidden "listenUsingInsecureRfcommOn(port)" method. |
| BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); |
| Method m = |
| adapter.getClass() |
| .getMethod("listenUsingInsecureRfcommOn", new Class[] {int.class}); |
| return (BluetoothServerSocket) m.invoke(adapter, port); |
| } |
| |
| private static int getPortNumber(BluetoothServerSocket serverSocket) throws Exception { |
| // Use reflection to reach the hidden "getChannel()" method. |
| Method m = serverSocket.getClass().getMethod("getChannel", new Class[0]); |
| return (int) m.invoke(serverSocket); |
| } |
| |
| private static String localMACAddress(VContext ctx) { |
| // TODO(suharshs): Android has disallowed getting the local address. |
| // This is a remaining working hack that gets the local bluetooth address, |
| // just to get things working. |
| return android.provider.Settings.Secure.getString( |
| V.getAndroidContext(ctx).getContentResolver(), "bluetooth_address"); |
| } |
| |
| private static String getMACAddress(VContext ctx, String btAddr) throws VException { |
| List<String> parts = Splitter.on("/").omitEmptyStrings().splitToList(btAddr); |
| switch (parts.size()) { |
| case 0: |
| throw new VException( |
| String.format( |
| "Couldn't split bluetooth address \"%s\" using \"/\" separator: " |
| + "got zero parts!", |
| btAddr)); |
| case 1: |
| return localMACAddress(ctx); |
| case 2: |
| String address = parts.get(0).toUpperCase(); |
| if (!BluetoothAdapter.checkBluetoothAddress(address)) { |
| throw new VException("Invalid bluetooth address: " + btAddr); |
| } |
| return address; |
| default: |
| throw new VException( |
| String.format( |
| "Couldn't parse bluetooth address \"%s\": too many \"/\".", |
| btAddr)); |
| } |
| } |
| |
| private static int getPortNumber(String btAddr) throws VException { |
| List<String> parts = Splitter.on("/").splitToList(btAddr); |
| switch (parts.size()) { |
| case 0: |
| throw new VException( |
| String.format( |
| "Couldn't split bluetooth address \"%s\" using \"/\" separator: " |
| + "got zero parts!", |
| btAddr)); |
| case 1: |
| case 2: |
| try { |
| int port = Integer.parseInt((parts.get(parts.size() - 1))); |
| if (port < 0 || port > 30) { |
| throw new VException( |
| String.format( |
| "Illegal port number %q in bluetooth " + "address \"%s\".", |
| port, btAddr)); |
| } |
| return port; |
| } catch (NumberFormatException e) { |
| throw new VException( |
| String.format( |
| "Couldn't parse port number in bluetooth address \"%s\": %s", |
| btAddr, e.getMessage())); |
| } |
| default: |
| throw new VException( |
| String.format( |
| "Couldn't parse bluetooth address \"%s\": too many \"/\".", |
| btAddr)); |
| } |
| } |
| |
| static class Listener { |
| private final BluetoothServerSocket serverSocket; |
| private final String localAddress; |
| |
| Listener(BluetoothServerSocket serverSocket, String address) { |
| this.serverSocket = serverSocket; |
| this.localAddress = address; |
| } |
| |
| Stream accept() throws IOException { |
| try { |
| BluetoothSocket socket = serverSocket.accept(); |
| // There is no way currently to retrieve the remote end's channel number, |
| // but that's probably OK. |
| String remoteAddress = |
| String.format("%s/%d", socket.getRemoteDevice().getAddress(), 0); |
| return new Stream(socket, localAddress, remoteAddress); |
| } catch (IOException e) { |
| serverSocket.close(); |
| throw e; |
| } |
| } |
| |
| void close() throws IOException { |
| serverSocket.close(); |
| } |
| |
| String address() { |
| return localAddress; |
| } |
| } |
| |
| static class Stream { |
| private final BluetoothSocket socket; |
| private final String localAddress; |
| private final String remoteAddress; |
| |
| Stream(BluetoothSocket socket, String localAddress, String remoteAddress) { |
| this.socket = socket; |
| this.localAddress = localAddress; |
| this.remoteAddress = remoteAddress; |
| } |
| |
| byte[] read(int n) throws IOException { |
| try { |
| InputStream in = socket.getInputStream(); |
| byte[] buf = new byte[n]; |
| int total = 0; |
| while (total < n) { |
| int r = in.read(buf, total, n - total); |
| if (r < 0) { |
| break; |
| } |
| total += r; |
| } |
| return total == n ? buf : Arrays.copyOf(buf, total); |
| } catch (IOException e) { |
| socket.close(); |
| throw e; |
| } |
| } |
| |
| void write(byte[] data) throws IOException { |
| try { |
| OutputStream out = socket.getOutputStream(); |
| out.write(data); |
| // TODO(jhahn): Do we need to flush for every write? |
| // out.flush(); |
| } catch (IOException e) { |
| socket.close(); |
| throw e; |
| } |
| } |
| |
| void close() throws IOException { |
| socket.close(); |
| } |
| |
| String localAddress() { |
| return this.localAddress; |
| } |
| |
| String remoteAddress() { |
| return this.remoteAddress; |
| } |
| } |
| } |