blob: 83c9da9b5334bf794e003aad87091d2b5c6deb89 [file] [log] [blame]
// 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 com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import org.joda.time.Duration;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
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 Bluetooth {
private static final List<Integer> BLUETOOTH_PORTS = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31);
static Listener listen(String btAddr) throws VException {
String macAddr = getMACAddress(btAddr);
int port = getPortNumber(btAddr);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (!macAddr.equals(adapter.getAddress())) {
throw new VException("Illegal MAC address to listen on: no local device found with "
+ "MAC address: " + macAddr + " (local address is: " + adapter.getAddress()
+ " )");
}
List<Integer> ports = null;
if (port == 0) { // listen on the first available port.
ports = new ArrayList(BLUETOOTH_PORTS);
Collections.shuffle(ports);
} else { // listen on a specific port only
ports = ImmutableList.of(port);
}
VException lastError = null;
for (int portNum : ports) {
try {
BluetoothServerSocket socket = listenOnPort(portNum);
return new Listener(socket, String.format("%s/%d", macAddr, portNum));
} catch (VException e) {
// OK, try the next one
lastError = e;
}
}
throw lastError;
}
static Connection dial(String btAddr, Duration timeout) throws VException {
String macAddr = getMACAddress(btAddr);
int port = getPortNumber(btAddr);
BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(macAddr);
try {
// 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.
Method m = device.getClass().getMethod("createInsecureRfcommSocket",
new Class[]{int.class});
final BluetoothSocket socket = (BluetoothSocket) m.invoke(device, port);
// Connect.
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
socket.close();
} catch (IOException e) {
System.err.println("Couldn't close BluetoothSocket.");
}
}
}, timeout.getMillis());
try {
socket.connect();
} catch (IOException e) {
throw new VException("Couldn't connect: " + e.getMessage());
} finally {
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",
BluetoothAdapter.getDefaultAdapter().getAddress(), 0);
String remoteAddr = String.format("%s/%d", macAddr, port);
return new Connection(socket, localAddr, remoteAddr);
} catch (Exception e) {
throw new VException("Couldn't invoke createInsecureRfcommSocket: "
+ e.getMessage());
}
}
private static BluetoothServerSocket listenOnPort(int port) throws VException {
// Use reflection to reach the hidden "listenUsingInsecureRfcommOn(port)" method.
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
try {
Method m = adapter.getClass().
getMethod("listenUsingInsecureRfcommOn", new Class[]{int.class});
return (BluetoothServerSocket) m.invoke(adapter, port);
} catch (Exception e) {
throw new VException("Error invoking listenUsingInsecureRfcommOn: "
+ e.getMessage());
}
}
private static String getMACAddress(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 BluetoothAdapter.getDefaultAdapter().getAddress();
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 > 32) {
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;
}
Connection accept() throws IOException {
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 Connection(socket, localAddress, remoteAddress);
}
void close() throws IOException {
serverSocket.close();
}
String address() {
return localAddress;
}
}
static class Connection {
private final BluetoothSocket socket;
private final String localAddress;
private final String remoteAddress;
Connection(BluetoothSocket socket, String localAddress, String remoteAddress) {
this.socket = socket;
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
}
byte[] read(int n) throws IOException {
byte[] buf = new byte[n];
int num = socket.getInputStream().read(buf);
return num == buf.length ? buf : Arrays.copyOf(buf, num);
}
synchronized void write(byte[] data) throws IOException {
socket.getOutputStream().write(data);
}
void close() throws IOException {
socket.close();
}
String localAddress() {
return this.localAddress;
}
String remoteAddress() {
return this.remoteAddress;
}
}
private Bluetooth() {}
}