Newer
Older
package org.briarproject.plugins.droidtooth;
import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE;
import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
import static android.bluetooth.BluetoothAdapter.STATE_OFF;
import static android.bluetooth.BluetoothAdapter.STATE_ON;
import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import org.briarproject.api.ContactId;
import org.briarproject.api.TransportId;
import org.briarproject.api.TransportProperties;
import org.briarproject.api.android.AndroidExecutor;
import org.briarproject.api.crypto.PseudoRandom;
import org.briarproject.api.plugins.duplex.DuplexPlugin;
import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
import org.briarproject.api.system.Clock;
import org.briarproject.util.LatchedReference;
import org.briarproject.util.StringUtils;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
class DroidtoothPlugin implements DuplexPlugin {
// Share an ID with the J2SE Bluetooth plugin
static final TransportId ID = new TransportId("bt");
private static final Logger LOG =
Logger.getLogger(DroidtoothPlugin.class.getName());
private static final int UUID_BYTES = 16;
private static final String FOUND =
"android.bluetooth.device.action.FOUND";
private static final String DISCOVERY_FINISHED =
"android.bluetooth.adapter.action.DISCOVERY_FINISHED";
private final Executor ioExecutor;
private final AndroidExecutor androidExecutor;
private final Context appContext;
private final SecureRandom secureRandom;
private final Clock clock;
private final DuplexPluginCallback callback;
private final int maxLatency, pollingInterval;
private volatile boolean running = false;
private volatile boolean wasDisabled = false;
private volatile BluetoothStateReceiver receiver = null;
private volatile BluetoothServerSocket socket = null;
// Non-null if the plugin started successfully
private volatile BluetoothAdapter adapter = null;
DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Clock clock,
DuplexPluginCallback callback, int maxLatency,
int pollingInterval) {
this.ioExecutor = ioExecutor;
this.androidExecutor = androidExecutor;
this.appContext = appContext;
this.maxLatency = maxLatency;
this.pollingInterval = pollingInterval;
}
public TransportId getId() {
return ID;
}
public int getMaxLatency() {
return maxLatency;
}
public int getMaxIdleTime() {
// Bluetooth detects dead connections so we don't need keepalives
return Integer.MAX_VALUE;
public boolean start() throws IOException {
// BluetoothAdapter.getDefaultAdapter() must be called on a thread
// with a message queue, so submit it to the AndroidExecutor
try {
adapter = androidExecutor.call(new Callable<BluetoothAdapter>() {
public BluetoothAdapter call() throws Exception {
return BluetoothAdapter.getDefaultAdapter();
}
});
Thread.currentThread().interrupt();
throw new IOException("Interrupted while getting BluetoothAdapter");
LOG.info("Bluetooth is not supported");
return false;
}
running = true;
// Listen for changes to the Bluetooth state
IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STATE_CHANGED);
filter.addAction(ACTION_SCAN_MODE_CHANGED);
receiver = new BluetoothStateReceiver();
appContext.registerReceiver(receiver, filter);
// If Bluetooth is enabled, bind a socket - otherwise enable it
} else if (callback.getConfig().getBoolean("enable", true)) {
if (adapter.enable()) LOG.info("Enabling Bluetooth");
else LOG.info("Could not enable Bluetooth");
} else {
LOG.info("Not enabling Bluetooth");
return true;
}
private void bind() {
ioExecutor.execute(new Runnable() {
public void run() {
if (!isRunning()) return;
if (LOG.isLoggable(INFO))
LOG.info("Local address " + adapter.getAddress());
// Advertise the Bluetooth address to contacts
TransportProperties p = new TransportProperties();
p.put("address", adapter.getAddress());
callback.mergeLocalProperties(p);
// Bind a server socket to accept connections from contacts
try {
ss = InsecureBluetooth.listen(adapter, "RFCOMM", getUuid());
} catch (IOException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
return;
}
tryToClose(ss);
return;
}
LOG.info("Socket bound");
socket = ss;
acceptContactConnections();
}
});
String uuid = callback.getLocalProperties().get("uuid");
byte[] random = new byte[UUID_BYTES];
secureRandom.nextBytes(random);
uuid = UUID.nameUUIDFromBytes(random).toString();
TransportProperties p = new TransportProperties();
p.put("uuid", uuid);
callback.mergeLocalProperties(p);
}
return UUID.fromString(uuid);
}
private void tryToClose(BluetoothServerSocket ss) {
try {
if (ss != null) ss.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
private void acceptContactConnections() {
s = socket.accept();
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());
String address = s.getRemoteDevice().getAddress();
LOG.info("Connection from " + address);
}
callback.incomingConnectionCreated(wrapSocket(s));
private DuplexTransportConnection wrapSocket(BluetoothSocket s) {
return new DroidtoothTransportConnection(this, s);
}
running = false;
if (receiver != null) appContext.unregisterReceiver(receiver);
tryToClose(socket);
// Disable Bluetooth if we enabled it and it's still enabled
if (wasDisabled && adapter.isEnabled()) {
if (adapter.disable()) LOG.info("Disabling Bluetooth");
else LOG.info("Could not disable Bluetooth");
public boolean isRunning() {
return running && adapter.isEnabled();
public boolean shouldPoll() {
return true;
}
public int getPollingInterval() {
return pollingInterval;
}
public void poll(Collection<ContactId> connected) {
// Try to connect to known devices in parallel
Map<ContactId, TransportProperties> remote =
callback.getRemoteProperties();
for (Entry<ContactId, TransportProperties> e : remote.entrySet()) {
if (connected.contains(c)) continue;
final String address = e.getValue().get("address");
if (StringUtils.isNullOrEmpty(address)) continue;
final String uuid = e.getValue().get("uuid");
if (StringUtils.isNullOrEmpty(uuid)) continue;
ioExecutor.execute(new Runnable() {
public void run() {
BluetoothSocket s = connect(address, uuid);
callback.outgoingConnectionCreated(c, wrapSocket(s));
}
});
private BluetoothSocket connect(String address, String uuid) {
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
if (LOG.isLoggable(WARNING))
LOG.warning("Invalid address " + address);
return null;
}
// Validate the UUID
UUID u;
try {
u = UUID.fromString(uuid);
} catch (IllegalArgumentException e) {
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
return null;
}
// Try to connect
BluetoothDevice d = adapter.getRemoteDevice(address);
s = InsecureBluetooth.createSocket(d, u);
if (LOG.isLoggable(INFO)) LOG.info("Connecting to " + address);
s.connect();
if (LOG.isLoggable(INFO)) LOG.info("Connected to " + address);
} catch (IOException e) {
if (LOG.isLoggable(INFO))
LOG.info("Failed to connect to " + address);
private void tryToClose(BluetoothSocket s) {
try {
if (s != null) s.close();
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
public DuplexTransportConnection createConnection(ContactId c) {
TransportProperties p = callback.getRemoteProperties().get(c);
if (StringUtils.isNullOrEmpty(address)) return null;
if (StringUtils.isNullOrEmpty(uuid)) return null;
BluetoothSocket s = connect(address, uuid);
return new DroidtoothTransportConnection(this, s);
}
public boolean supportsInvitations() {
return true;
}
public DuplexTransportConnection createInvitationConnection(PseudoRandom r,
// Use the invitation codes to generate the UUID
byte[] b = r.nextBytes(UUID_BYTES);
UUID uuid = UUID.nameUUIDFromBytes(b);
if (LOG.isLoggable(INFO)) LOG.info("Invitation UUID " + uuid);
// Bind a server socket for receiving invitation connections
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid);
} catch (IOException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
// Start the background threads
LatchedReference<BluetoothSocket> socketLatch =
new LatchedReference<BluetoothSocket>();
new DiscoveryThread(socketLatch, uuid.toString(), timeout).start();
new BluetoothListenerThread(socketLatch, ss).start();
// Wait for an incoming or outgoing connection
BluetoothSocket s = socketLatch.waitForReference(timeout);
if (s != null) return new DroidtoothTransportConnection(this, s);
} catch (InterruptedException e) {
LOG.warning("Interrupted while exchanging invitations");
Thread.currentThread().interrupt();
// Closing the socket will terminate the listener thread
return null;
}
private class BluetoothStateReceiver extends BroadcastReceiver {
public void onReceive(Context ctx, Intent intent) {
int state = intent.getIntExtra(EXTRA_STATE, 0);
LOG.info("Bluetooth enabled");
bind();
LOG.info("Bluetooth disabled");
tryToClose(socket);
int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0);
LOG.info("Scan mode: None");
} else if (scanMode == SCAN_MODE_CONNECTABLE) {
LOG.info("Scan mode: Connectable");
} else if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
LOG.info("Scan mode: Discoverable");
}
private class DiscoveryThread extends Thread {
private final LatchedReference<BluetoothSocket> socketLatch;
private final String uuid;
private final long timeout;
private DiscoveryThread(LatchedReference<BluetoothSocket> socketLatch,
String uuid, long timeout) {
this.socketLatch = socketLatch;
this.timeout = timeout;
}
@Override
public void run() {
long end = clock.currentTimeMillis() + timeout;
// Discover nearby devices
LOG.info("Discovering nearby devices");
List<String> addresses;
try {
addresses = discoverDevices(end - now);
LOG.warning("Interrupted while discovering devices");
Thread.currentThread().interrupt();
LOG.info("No devices discovered");
continue;
}
// Connect to any device with the right UUID
for (String address : addresses) {
if (finished(end)) return;
BluetoothSocket s = connect(address, uuid);
LOG.info("Outgoing connection");
LOG.info("Closing redundant connection");
tryToClose(s);
}
return;
}
}
}
}
private boolean finished(long end) {
long now = clock.currentTimeMillis();
return now >= end || !isRunning() || socketLatch.isSet();
}
private List<String> discoverDevices(long timeout)
throws InterruptedException {
IntentFilter filter = new IntentFilter();
filter.addAction(FOUND);
filter.addAction(DISCOVERY_FINISHED);
DiscoveryReceiver disco = new DiscoveryReceiver();
appContext.registerReceiver(disco, filter);
LOG.info("Starting discovery");
adapter.startDiscovery();
return disco.waitForAddresses(timeout);
private static class DiscoveryReceiver extends BroadcastReceiver {
private final CountDownLatch finished = new CountDownLatch(1);
private final List<String> addresses = new ArrayList<String>();
public void onReceive(Context ctx, Intent intent) {
if (action.equals(DISCOVERY_FINISHED)) {
LOG.info("Discovery finished");
ctx.unregisterReceiver(this);
finished.countDown();
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
LOG.info("Discovered device: " + d.getAddress());
addresses.add(d.getAddress());
private List<String> waitForAddresses(long timeout)
throws InterruptedException {
finished.await(timeout, MILLISECONDS);
return Collections.unmodifiableList(addresses);
}
}
private static class BluetoothListenerThread extends Thread {
private final LatchedReference<BluetoothSocket> socketLatch;
private final BluetoothServerSocket serverSocket;
private BluetoothListenerThread(
LatchedReference<BluetoothSocket> socketLatch,
BluetoothServerSocket serverSocket) {
this.socketLatch = socketLatch;
this.serverSocket = serverSocket;
}
@Override
public void run() {
try {
BluetoothSocket s = serverSocket.accept();
LOG.info("Incoming connection");
LOG.info("Closing redundant connection");
// This is expected when the socket is closed
if (LOG.isLoggable(INFO)) LOG.info(e.toString());