io.v.android.impl.google.rpc.protocols.bt.BluetoothWithPort.java Source code

Java tutorial

Introduction

Here is the source code for io.v.android.impl.google.rpc.protocols.bt.BluetoothWithPort.java

Source

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