rpc/bt: add Bluetooth with SDP socket
* Add Bluetooth with SDP socket, which does not use any hidden methods
* An initial connection can be slower than socket with port due to SDP.
MultiPart: 1/2
Change-Id: Ia0aeb0b758af985a4db114ed8c36077715778341
diff --git a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/BluetoothWithPort.java
similarity index 99%
rename from android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
rename to android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/BluetoothWithPort.java
index 87ed359..03a10fe 100644
--- a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/Bluetooth.java
+++ b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/BluetoothWithPort.java
@@ -33,7 +33,7 @@
* Used as a helper class for native code which sets up and registers the bluetooth protocol with
* the vanadium RPC service.
*/
-class Bluetooth {
+class BluetoothWithPort {
private static final String TAG = "Bluetooth";
static Listener listen(VContext ctx, String btAddr) throws Exception {
@@ -273,6 +273,4 @@
return this.remoteAddress;
}
}
-
- private Bluetooth() {}
}
diff --git a/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/BluetoothWithSdp.java b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/BluetoothWithSdp.java
new file mode 100644
index 0000000..8bdbb43
--- /dev/null
+++ b/android-lib/src/main/java/io/v/android/impl/google/rpc/protocols/bt/BluetoothWithSdp.java
@@ -0,0 +1,323 @@
+// Copyright 2016 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Random;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.UUID;
+
+import io.v.android.v23.V;
+import io.v.v23.context.VContext;
+
+/**
+ * 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.
+ */
+public class BluetoothWithSdp {
+ private static final String TAG = "Bluetooth";
+
+ private static final String SDP_NAME = "v23";
+ // Generated by UUID5(UUID5(NULL, "v.io"), "_bluetooth_socket_port")
+ private static final UUID BASE_SDP_UUID =
+ UUID.fromString("0f83a207-7f39-57c4-92f6-bd46039a5540");
+
+ private static final int MAX_PORT = 30;
+ private static final int PORT_MASK = 0x31;
+
+ private static final List<Integer> sPorts;
+
+ static {
+ sPorts = new ArrayList<>();
+ for (int i = 1; i <= MAX_PORT; i++) {
+ sPorts.add(i);
+ }
+ // Shuffle port numbers to prevent peers from using cached SDP records.
+ Collections.shuffle(sPorts, new Random(System.currentTimeMillis()));
+ }
+
+ private static synchronized int getServerPort(int port) throws IOException {
+ if (port == 0) {
+ if (sPorts.isEmpty()) {
+ throw new IOException("No more ports available");
+ }
+ port = sPorts.get(0);
+ }
+ if (!sPorts.remove(new Integer(port))) {
+ throw new IOException(String.format("Port %d not available", port));
+ }
+ return port;
+ }
+
+ private static synchronized void putServerPort(int port) {
+ if (port > 0 && port <= MAX_PORT && !sPorts.contains(port)) {
+ sPorts.add(port);
+ }
+ }
+
+ private static UUID getSdpUuidFromPort(int port) {
+ if (port <= 0 || port > MAX_PORT) {
+ throw new IllegalArgumentException(String.format("Illegal port number %d", port));
+ }
+ return new UUID(
+ BASE_SDP_UUID.getMostSignificantBits(),
+ BASE_SDP_UUID.getLeastSignificantBits() | (long) port);
+ }
+
+ private static String getLocalMacAddress(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 address) {
+ List<String> parts = Splitter.on("/").omitEmptyStrings().splitToList(address);
+ switch (parts.size()) {
+ case 0:
+ throw new IllegalArgumentException(
+ String.format(
+ "Couldn't split bluetooth address \"%s\" using \"/\" separator: "
+ + "got zero parts!",
+ address));
+ case 1:
+ return getLocalMacAddress(ctx);
+ case 2:
+ String macAddress = parts.get(0).toUpperCase();
+ if (!BluetoothAdapter.checkBluetoothAddress(macAddress)) {
+ throw new IllegalArgumentException("Invalid bluetooth address: " + address);
+ }
+ return macAddress;
+ default:
+ throw new IllegalArgumentException(
+ String.format(
+ "Couldn't parse bluetooth address \"%s\": too many \"/\".",
+ address));
+ }
+ }
+
+ private static int getPortNumber(String address) {
+ List<String> parts = Splitter.on("/").splitToList(address);
+ switch (parts.size()) {
+ case 0:
+ throw new IllegalArgumentException(
+ String.format(
+ "Couldn't split bluetooth address \"%s\" using \"/\" separator: "
+ + "got zero parts!",
+ address));
+ case 1:
+ case 2:
+ int port = Integer.parseInt((parts.get(parts.size() - 1)));
+ if (port < 0 || port > MAX_PORT) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Illegal port number %q in bluetooth " + "address \"%s\".",
+ port, address));
+ }
+ return port;
+ default:
+ throw new IllegalArgumentException(
+ String.format(
+ "Couldn't parse bluetooth address \"%s\": too many \"/\".",
+ address));
+ }
+ }
+
+ static Listener listen(VContext ctx, String address) throws Exception {
+ String macAddress = getMacAddress(ctx, address);
+ int port = getPortNumber(address);
+ return new Listener(macAddress, port);
+ }
+
+ static Stream dial(VContext ctx, String address, Duration timeout) throws Exception {
+ String macAddress = getMacAddress(ctx, address);
+ int port = getPortNumber(address);
+
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ throw new IOException("BluetoothAdapter not available");
+ }
+ BluetoothDevice device = adapter.getRemoteDevice(macAddress);
+
+ UUID uuid = getSdpUuidFromPort(port);
+ final BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(uuid);
+
+ 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 localAddress = String.format("%s/0", getLocalMacAddress(ctx));
+ String remoteAddress = String.format("%s/%d", macAddress, port);
+ return new Stream(socket, localAddress, remoteAddress);
+ }
+
+ // Listener provides methods for accepting new Bluetooth connections.
+ public static class Listener {
+ private final String mLocalAddress;
+
+ private int mPort;
+ private BluetoothServerSocket mServerSocket;
+
+ private Listener(String macAddress, int port) throws IOException {
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter == null) {
+ throw new IOException("BluetoothAdapter not available");
+ }
+
+ mPort = getServerPort(port);
+ mLocalAddress = String.format("%s/%d", macAddress, mPort);
+
+ Log.d(TAG, String.format("listening on port %d", mPort));
+
+ try {
+ UUID uuid = getSdpUuidFromPort(mPort);
+ mServerSocket = adapter.listenUsingInsecureRfcommWithServiceRecord(SDP_NAME, uuid);
+ } catch (IOException e) {
+ close();
+ throw e;
+ }
+ }
+
+ public Stream accept() throws IOException {
+ // 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.
+ //
+ // TODO(jhahn,suharshs): Is this true?
+ try {
+ BluetoothSocket socket = mServerSocket.accept();
+ // There is no way currently to retrieve the remote end's channel number,
+ // but that's probably OK.
+ String remoteAddress = String.format("%s/0", socket.getRemoteDevice().getAddress());
+ return new Stream(socket, mLocalAddress, remoteAddress);
+ } catch (IOException e) {
+ close();
+ throw e;
+ }
+ }
+
+ public synchronized void close() throws IOException {
+ if (mPort > 0) {
+ putServerPort(mPort);
+ mPort = 0;
+ }
+ if (mServerSocket != null) {
+ mServerSocket.close();
+ mServerSocket = null;
+ }
+ }
+
+ public String address() {
+ return mLocalAddress;
+ }
+
+ protected void finalize() {
+ try {
+ close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ // Stream provides I/O primitives to read and write over a Bluetooth socket.
+ public static class Stream {
+ private final BluetoothSocket mSocket;
+ private final String mLocalAddress;
+ private final String mRemoteAddress;
+
+ private Stream(BluetoothSocket socket, String localAddress, String remoteAddress) {
+ mSocket = socket;
+ mLocalAddress = localAddress;
+ mRemoteAddress = remoteAddress;
+ }
+
+ public byte[] read(int n) throws IOException {
+ try {
+ InputStream in = mSocket.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) {
+ close();
+ throw e;
+ }
+ }
+
+ public void write(byte[] data) throws IOException {
+ try {
+ // TODO(jhahn): Do we need to flush for every write?
+ OutputStream out = mSocket.getOutputStream();
+ out.write(data);
+ } catch (IOException e) {
+ close();
+ throw e;
+ }
+ }
+
+ public void close() throws IOException {
+ mSocket.close();
+ }
+
+ public String localAddress() {
+ return mLocalAddress;
+ }
+
+ public String remoteAddress() {
+ return mRemoteAddress;
+ }
+ }
+}