| // 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; |
| } |
| } |
| } |