blob: 269c286e7860fc362d40fb3eb3a7b0d662c1fded [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 android.util.Log;
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 java.util.concurrent.Executor;
import io.v.impl.google.rt.VRuntimeImpl;
import io.v.android.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.rpc.Callback;
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 String TAG = "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(VContext ctx, String btAddr) throws VException {
String macAddr = getMACAddress(ctx, btAddr);
int port = getPortNumber(btAddr);
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
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);
}
Executor executor = VRuntimeImpl.getRuntimeExecutor(ctx);
if (executor == null) {
throw new VException("NULL executor in context: did you derive this context from " +
"the context returned by V.init()?");
}
VException lastError = null;
for (int portNum : ports) {
try {
BluetoothServerSocket socket = listenOnPort(portNum);
Log.d(TAG, String.format("listening on port %d", portNum));
return new Listener(executor, socket, String.format("%s/%d", macAddr, portNum));
} catch (VException e) {
// OK, try the next one
lastError = e;
}
}
throw lastError;
}
static void dial(VContext ctx, String btAddr, final Duration timeout,
final Callback<Stream> callback) throws VException {
final String macAddr = getMACAddress(ctx, btAddr);
final int port = getPortNumber(btAddr);
final Executor executor = VRuntimeImpl.getRuntimeExecutor(ctx);
if (executor == null) {
throw new VException("NULL executor in context: did you derive this context from " +
"the context returned by V.init()?");
}
executor.execute(new Runnable() {
@Override
public void run() {
final 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, 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) {
System.err.println("Couldn't close BluetoothSocket.");
}
}
}, timeout.getMillis());
}
try {
socket.connect();
} catch (IOException e) {
callback.onFailure(new VException("Couldn't connect: " + e.getMessage()));
} 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);
callback.onSuccess(new Stream(executor, socket, localAddr, remoteAddr));
} catch (Exception e) {
callback.onFailure(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 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 > 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 Executor executor;
private final BluetoothServerSocket serverSocket;
private final String localAddress;
Listener(Executor executor, BluetoothServerSocket serverSocket, String address) {
this.executor = executor;
this.serverSocket = serverSocket;
this.localAddress = address;
}
void accept(final Callback<Stream> callback) {
executor.execute(new Runnable() {
@Override
public void run() {
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);
callback.onSuccess(new Stream(
executor, socket, localAddress, remoteAddress));
} catch (IOException e) {
callback.onFailure(new VException(e.getMessage()));
}
}
});
}
void close() throws IOException {
serverSocket.close();
}
String address() {
return localAddress;
}
}
static class Stream {
private final Executor executor;
private final BluetoothSocket socket;
private final String localAddress;
private final String remoteAddress;
Stream(Executor executor, BluetoothSocket socket, String localAddress,
String remoteAddress) {
this.executor = executor;
this.socket = socket;
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
}
void read(final int n, final Callback<byte[]> callback) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
byte[] buf = new byte[n];
int num = socket.getInputStream().read(buf);
callback.onSuccess(num == buf.length ? buf : Arrays.copyOf(buf, num));
} catch (IOException e) {
callback.onFailure(new VException(e.getMessage()));
}
}
});
}
void write(final byte[] data, final Callback<Void> callback) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
socket.getOutputStream().write(data);
callback.onSuccess(null);
} catch (IOException e) {
callback.onFailure(new VException(e.getMessage()));
}
}
});
}
void close() throws IOException {
socket.close();
}
String localAddress() {
return this.localAddress;
}
String remoteAddress() {
return this.remoteAddress;
}
}
private Bluetooth() {}
}