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

Java tutorial

Introduction

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

Source

// Copyright 2016 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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

import io.v.android.v23.V;
import io.v.v23.context.VContext;

/**
 * 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.
 */
public class BluetoothWithSdp {
    private static final String TAG = "Bluetooth";

    private static final String SDP_NAME = "v23";
    // Generated by UUID5(UUID5(NULL, "v.io"), "_bluetooth_socket_port")
    private static final UUID BASE_SDP_UUID = UUID.fromString("0f83a207-7f39-57c4-92f6-bd46039a5540");

    private static final int MAX_PORT = 30;
    private static final int PORT_MASK = 0x31;

    private static final List<Integer> sPorts;

    static {
        sPorts = new ArrayList<>();
        for (int i = 1; i <= MAX_PORT; i++) {
            sPorts.add(i);
        }
        // Shuffle port numbers to prevent peers from using cached SDP records.
        Collections.shuffle(sPorts, new Random(System.currentTimeMillis()));
    }

    private static synchronized int getServerPort(int port) throws IOException {
        if (port == 0) {
            if (sPorts.isEmpty()) {
                throw new IOException("No more ports available");
            }
            port = sPorts.get(0);
        }
        if (!sPorts.remove(new Integer(port))) {
            throw new IOException(String.format("Port %d not available", port));
        }
        return port;
    }

    private static synchronized void putServerPort(int port) {
        if (port > 0 && port <= MAX_PORT && !sPorts.contains(port)) {
            sPorts.add(port);
        }
    }

    private static UUID getSdpUuidFromPort(int port) {
        if (port <= 0 || port > MAX_PORT) {
            throw new IllegalArgumentException(String.format("Illegal port number %d", port));
        }
        return new UUID(BASE_SDP_UUID.getMostSignificantBits(),
                BASE_SDP_UUID.getLeastSignificantBits() | (long) port);
    }

    private static String getLocalMacAddress(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 address) {
        List<String> parts = Splitter.on("/").omitEmptyStrings().splitToList(address);
        switch (parts.size()) {
        case 0:
            throw new IllegalArgumentException(String.format(
                    "Couldn't split bluetooth address \"%s\" using \"/\" separator: " + "got zero parts!",
                    address));
        case 1:
            return getLocalMacAddress(ctx);
        case 2:
            String macAddress = parts.get(0).toUpperCase();
            if (!BluetoothAdapter.checkBluetoothAddress(macAddress)) {
                throw new IllegalArgumentException("Invalid bluetooth address: " + address);
            }
            return macAddress;
        default:
            throw new IllegalArgumentException(
                    String.format("Couldn't parse bluetooth address \"%s\": too many \"/\".", address));
        }
    }

    private static int getPortNumber(String address) {
        List<String> parts = Splitter.on("/").splitToList(address);
        switch (parts.size()) {
        case 0:
            throw new IllegalArgumentException(String.format(
                    "Couldn't split bluetooth address \"%s\" using \"/\" separator: " + "got zero parts!",
                    address));
        case 1:
        case 2:
            int port = Integer.parseInt((parts.get(parts.size() - 1)));
            if (port < 0 || port > MAX_PORT) {
                throw new IllegalArgumentException(
                        String.format("Illegal port number %q in bluetooth " + "address \"%s\".", port, address));
            }
            return port;
        default:
            throw new IllegalArgumentException(
                    String.format("Couldn't parse bluetooth address \"%s\": too many \"/\".", address));
        }
    }

    static Listener listen(VContext ctx, String address) throws Exception {
        String macAddress = getMacAddress(ctx, address);
        int port = getPortNumber(address);
        return new Listener(macAddress, port);
    }

    static Stream dial(VContext ctx, String address, Duration timeout) throws Exception {
        String macAddress = getMacAddress(ctx, address);
        int port = getPortNumber(address);

        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
        if (adapter == null) {
            throw new IOException("BluetoothAdapter not available");
        }
        BluetoothDevice device = adapter.getRemoteDevice(macAddress);

        UUID uuid = getSdpUuidFromPort(port);
        final BluetoothSocket socket = device.createInsecureRfcommSocketToServiceRecord(uuid);

        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 localAddress = String.format("%s/0", getLocalMacAddress(ctx));
        String remoteAddress = String.format("%s/%d", macAddress, port);
        return new Stream(socket, localAddress, remoteAddress);
    }

    // Listener provides methods for accepting new Bluetooth connections.
    public static class Listener {
        private final String mLocalAddress;

        private int mPort;
        private BluetoothServerSocket mServerSocket;

        private Listener(String macAddress, int port) throws IOException {
            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
            if (adapter == null) {
                throw new IOException("BluetoothAdapter not available");
            }

            mPort = getServerPort(port);
            mLocalAddress = String.format("%s/%d", macAddress, mPort);

            Log.d(TAG, String.format("listening on port %d", mPort));

            try {
                UUID uuid = getSdpUuidFromPort(mPort);
                mServerSocket = adapter.listenUsingInsecureRfcommWithServiceRecord(SDP_NAME, uuid);
            } catch (IOException e) {
                close();
                throw e;
            }
        }

        public Stream accept() throws IOException {
            //  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.
            //
            //  TODO(jhahn,suharshs): Is this true?
            try {
                BluetoothSocket socket = mServerSocket.accept();
                // There is no way currently to retrieve the remote end's channel number,
                // but that's probably OK.
                String remoteAddress = String.format("%s/0", socket.getRemoteDevice().getAddress());
                return new Stream(socket, mLocalAddress, remoteAddress);
            } catch (IOException e) {
                close();
                throw e;
            }
        }

        public synchronized void close() throws IOException {
            if (mPort > 0) {
                putServerPort(mPort);
                mPort = 0;
            }
            if (mServerSocket != null) {
                mServerSocket.close();
                mServerSocket = null;
            }
        }

        public String address() {
            return mLocalAddress;
        }

        protected void finalize() {
            try {
                close();
            } catch (IOException e) {
            }
        }
    }

    // Stream provides I/O primitives to read and write over a Bluetooth socket.
    public static class Stream {
        private final BluetoothSocket mSocket;
        private final String mLocalAddress;
        private final String mRemoteAddress;

        private Stream(BluetoothSocket socket, String localAddress, String remoteAddress) {
            mSocket = socket;
            mLocalAddress = localAddress;
            mRemoteAddress = remoteAddress;
        }

        public byte[] read(int n) throws IOException {
            try {
                InputStream in = mSocket.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) {
                close();
                throw e;
            }
        }

        public void write(byte[] data) throws IOException {
            try {
                // TODO(jhahn): Do we need to flush for every write?
                OutputStream out = mSocket.getOutputStream();
                out.write(data);
            } catch (IOException e) {
                close();
                throw e;
            }
        }

        public void close() throws IOException {
            mSocket.close();
        }

        public String localAddress() {
            return mLocalAddress;
        }

        public String remoteAddress() {
            return mRemoteAddress;
        }
    }
}