blob: 03a10fe4196ceaf68d11f2ad0b028bdd817dae44 [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 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;
}
}
}