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