package net.sf.briar.plugins.bluetooth; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javax.bluetooth.BluetoothStateException; import javax.bluetooth.DiscoveryAgent; import javax.bluetooth.LocalDevice; import javax.microedition.io.Connector; import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnectionNotifier; import net.sf.briar.api.ContactId; import net.sf.briar.api.TransportProperties; import net.sf.briar.api.clock.Clock; import net.sf.briar.api.crypto.PseudoRandom; import net.sf.briar.api.plugins.PluginExecutor; import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; import net.sf.briar.api.protocol.TransportId; import net.sf.briar.util.OsUtils; import net.sf.briar.util.StringUtils; class BluetoothPlugin implements DuplexPlugin { public static final byte[] TRANSPORT_ID = StringUtils.fromHexString("d99c9313c04417dcf22fc60d12a187ea" + "00a539fd260f08a13a0d8a900cde5e49" + "1b4df2ffd42e40c408f2db7868f518aa"); private static final TransportId ID = new TransportId(TRANSPORT_ID); private static final Logger LOG = Logger.getLogger(BluetoothPlugin.class.getName()); private final Executor pluginExecutor; private final Clock clock; private final DuplexPluginCallback callback; private final long pollingInterval; private final Object discoveryLock = new Object(); private final Object localPropertiesLock = new Object(); private final ScheduledExecutorService scheduler; private final Collection sockets; // Locking: this private boolean running = false; // Locking: this private LocalDevice localDevice = null; // Locking: this private StreamConnectionNotifier socket = null; // Locking: this BluetoothPlugin(@PluginExecutor Executor pluginExecutor, Clock clock, DuplexPluginCallback callback, long pollingInterval) { this.pluginExecutor = pluginExecutor; this.clock = clock; this.callback = callback; this.pollingInterval = pollingInterval; scheduler = Executors.newScheduledThreadPool(0); sockets = new ArrayList(); } public TransportId getId() { return ID; } public void start() throws IOException { // Initialise the Bluetooth stack LocalDevice localDevice; try { localDevice = LocalDevice.getLocalDevice(); } catch(UnsatisfiedLinkError e) { // On Linux the user may need to install libbluetooth-dev if(OsUtils.isLinux()) callback.showMessage("BLUETOOTH_INSTALL_LIBS"); throw new IOException(e.toString()); } if(LOG.isLoggable(Level.INFO)) LOG.info("Address " + localDevice.getBluetoothAddress()); synchronized(this) { running = true; this.localDevice = localDevice; } pluginExecutor.execute(new Runnable() { public void run() { bind(); } }); } private void bind() { synchronized(this) { if(!running) return; } makeDeviceDiscoverable(); String url = "btspp://localhost:" + getUuid() + ";name=RFCOMM"; StreamConnectionNotifier scn; try { scn = (StreamConnectionNotifier) Connector.open(url); } catch(IOException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); return; } synchronized(this) { if(!running) { tryToClose(scn); return; } socket = scn; } acceptContactConnections(scn); } private String getUuid() { // FIXME: Avoid making alien calls with a lock held synchronized(localPropertiesLock) { TransportProperties p = callback.getLocalProperties(); String uuid = p.get("uuid"); if(uuid == null) { // Generate a (weakly) random UUID and store it byte[] b = new byte[16]; new Random().nextBytes(b); uuid = generateUuid(b); p.put("uuid", uuid); callback.setLocalProperties(p); } return uuid; } } private void makeDeviceDiscoverable() { // Try to make the device discoverable (requires root on Linux) LocalDevice localDevice; synchronized(this) { if(!running) return; localDevice = this.localDevice; } try { localDevice.setDiscoverable(DiscoveryAgent.GIAC); } catch(BluetoothStateException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); } // Advertise the address to contacts if the device is discoverable if(localDevice.getDiscoverable() != DiscoveryAgent.NOT_DISCOVERABLE) { // FIXME: Avoid making alien calls with a lock held synchronized(localPropertiesLock) { TransportProperties p = callback.getLocalProperties(); p.put("address", localDevice.getBluetoothAddress()); callback.setLocalProperties(p); } } } private void tryToClose(StreamConnectionNotifier scn) { try { scn.close(); } catch(IOException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); } } private void acceptContactConnections(StreamConnectionNotifier scn) { while(true) { StreamConnection s; try { s = scn.acceptAndOpen(); } catch(IOException e) { // This is expected when the socket is closed if(LOG.isLoggable(Level.INFO)) LOG.info(e.toString()); tryToClose(scn); return; } BluetoothTransportConnection conn = new BluetoothTransportConnection(s); callback.incomingConnectionCreated(conn); synchronized(this) { if(!running) return; } } } public void stop() { synchronized(this) { running = false; localDevice = null; for(StreamConnectionNotifier scn : sockets) tryToClose(scn); sockets.clear(); if(socket != null) { tryToClose(socket); socket = null; } } scheduler.shutdownNow(); } public boolean shouldPoll() { return true; } public long getPollingInterval() { return pollingInterval; } public void poll(final Collection connected) { synchronized(this) { if(!running) return; } pluginExecutor.execute(new Runnable() { public void run() { connectAndCallBack(connected); } }); } private void connectAndCallBack(Collection connected) { synchronized(this) { if(!running) return; } Map remote = callback.getRemoteProperties(); Map discovered = discoverContactUrls(remote); for(Entry e : discovered.entrySet()) { ContactId c = e.getKey(); // Don't create redundant connections if(connected.contains(c)) continue; String url = e.getValue(); DuplexTransportConnection d = connect(c, url); if(d != null) callback.outgoingConnectionCreated(c, d); } } private Map discoverContactUrls( Map remote) { LocalDevice localDevice; synchronized(this) { if(!running) return Collections.emptyMap(); localDevice = this.localDevice; } DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent(); Map addresses = new HashMap(); Map uuids = new HashMap(); for(Entry e : remote.entrySet()) { ContactId c = e.getKey(); TransportProperties p = e.getValue(); String address = p.get("address"); String uuid = p.get("uuid"); if(address != null && uuid != null) { addresses.put(address, c); uuids.put(c, uuid); } } if(addresses.isEmpty()) return Collections.emptyMap(); ContactListener listener = new ContactListener(discoveryAgent, Collections.unmodifiableMap(addresses), Collections.unmodifiableMap(uuids)); // FIXME: Avoid making alien calls with a lock held synchronized(discoveryLock) { try { discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener); return listener.waitForUrls(); } catch(BluetoothStateException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); return Collections.emptyMap(); } catch(InterruptedException e) { if(LOG.isLoggable(Level.INFO)) LOG.info("Interrupted while waiting for URLs"); Thread.currentThread().interrupt(); return Collections.emptyMap(); } } } private DuplexTransportConnection connect(ContactId c, String url) { synchronized(this) { if(!running) return null; } try { StreamConnection s = (StreamConnection) Connector.open(url); return new BluetoothTransportConnection(s); } catch(IOException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); return null; } } public DuplexTransportConnection createConnection(ContactId c) { synchronized(this) { if(!running) return null; } Map remote = callback.getRemoteProperties(); if(!remote.containsKey(c)) return null; remote = Collections.singletonMap(c, remote.get(c)); String url = discoverContactUrls(remote).get(c); return url == null ? null : connect(c, url); } public boolean supportsInvitations() { return true; } public DuplexTransportConnection sendInvitation(PseudoRandom r, long timeout) { return createInvitationConnection(r, timeout); } public DuplexTransportConnection acceptInvitation(PseudoRandom r, long timeout) { return createInvitationConnection(r, timeout); } private DuplexTransportConnection createInvitationConnection(PseudoRandom r, long timeout) { synchronized(this) { if(!running) return null; } // Use the invitation code to generate the UUID String uuid = generateUuid(r.nextBytes(16)); // The invitee's device may not be discoverable, so both parties must // try to initiate connections final ConnectionCallback c = new ConnectionCallback(uuid, timeout); pluginExecutor.execute(new Runnable() { public void run() { createInvitationConnection(c); } }); pluginExecutor.execute(new Runnable() { public void run() { bindInvitationSocket(c); } }); try { StreamConnection s = c.waitForConnection(); return s == null ? null : new BluetoothTransportConnection(s); } catch(InterruptedException e) { if(LOG.isLoggable(Level.INFO)) LOG.info("Interrupted while waiting for connection"); Thread.currentThread().interrupt(); return null; } } private String generateUuid(byte[] b) { UUID uuid = UUID.nameUUIDFromBytes(b); return uuid.toString().replaceAll("-", ""); } private void createInvitationConnection(ConnectionCallback c) { LocalDevice localDevice; synchronized(this) { if(!running) return; localDevice = this.localDevice; } DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent(); // Try to discover the other party until the invitation times out long end = clock.currentTimeMillis() + c.getTimeout(); String url = null; while(url == null && clock.currentTimeMillis() < end) { InvitationListener listener = new InvitationListener(discoveryAgent, c.getUuid()); // FIXME: Avoid making alien calls with a lock held synchronized(discoveryLock) { try { discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener); url = listener.waitForUrl(); } catch(BluetoothStateException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); return; } catch(InterruptedException e) { if(LOG.isLoggable(Level.INFO)) LOG.info("Interrupted while waiting for URL"); Thread.currentThread().interrupt(); return; } } synchronized(this) { if(!running) return; } } if(url == null) return; // Try to connect to the other party try { StreamConnection s = (StreamConnection) Connector.open(url); c.addConnection(s); } catch(IOException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); } } private void bindInvitationSocket(final ConnectionCallback c) { synchronized(this) { if(!running) return; } makeDeviceDiscoverable(); String url = "btspp://localhost:" + c.getUuid() + ";name=RFCOMM"; final StreamConnectionNotifier scn; try { scn = (StreamConnectionNotifier) Connector.open(url); } catch(IOException e) { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString()); return; } synchronized(this) { if(!running) { tryToClose(scn); return; } sockets.add(scn); } // Close the socket when the invitation times out Runnable close = new Runnable() { public void run() { synchronized(this) { sockets.remove(scn); } tryToClose(scn); } }; ScheduledFuture future = scheduler.schedule(close, c.getTimeout(), TimeUnit.MILLISECONDS); // Try to accept a connection try { StreamConnection s = scn.acceptAndOpen(); // Close the socket and return the connection if(future.cancel(false)) { synchronized(this) { sockets.remove(scn); } tryToClose(scn); } c.addConnection(s); } catch(IOException e) { // This is expected when the socket is closed if(LOG.isLoggable(Level.INFO)) LOG.info(e.toString()); tryToClose(scn); } } }