| // 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() {} |
| } |