mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
405 lines
11 KiB
Java
405 lines
11 KiB
Java
package net.sf.briar.plugins.bluetooth;
|
|
|
|
import java.io.IOException;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
import java.util.Random;
|
|
import java.util.concurrent.Executor;
|
|
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.TransportConfig;
|
|
import net.sf.briar.api.TransportId;
|
|
import net.sf.briar.api.TransportProperties;
|
|
import net.sf.briar.api.plugins.StreamPlugin;
|
|
import net.sf.briar.api.plugins.StreamPluginCallback;
|
|
import net.sf.briar.api.transport.StreamTransportConnection;
|
|
import net.sf.briar.plugins.AbstractPlugin;
|
|
import net.sf.briar.util.OsUtils;
|
|
import net.sf.briar.util.StringUtils;
|
|
|
|
class BluetoothPlugin extends AbstractPlugin implements StreamPlugin {
|
|
|
|
public static final int TRANSPORT_ID = 2;
|
|
|
|
private static final TransportId id = new TransportId(TRANSPORT_ID);
|
|
private static final Logger LOG =
|
|
Logger.getLogger(BluetoothPlugin.class.getName());
|
|
|
|
private final Object discoveryLock = new Object();
|
|
private final StreamPluginCallback callback;
|
|
private final long pollingInterval;
|
|
|
|
private LocalDevice localDevice = null; // Locking: this
|
|
private StreamConnectionNotifier socket = null; // Locking: this
|
|
|
|
BluetoothPlugin(Executor executor, StreamPluginCallback callback,
|
|
long pollingInterval) {
|
|
super(executor);
|
|
this.callback = callback;
|
|
this.pollingInterval = pollingInterval;
|
|
}
|
|
|
|
public TransportId getId() {
|
|
return id;
|
|
}
|
|
|
|
@Override
|
|
public void start() throws IOException {
|
|
// Initialise the Bluetooth stack
|
|
try {
|
|
synchronized(this) {
|
|
super.start();
|
|
localDevice = LocalDevice.getLocalDevice();
|
|
}
|
|
} catch(UnsatisfiedLinkError e) {
|
|
// On Linux the user may need to install libbluetooth-dev
|
|
if(OsUtils.isLinux()) {
|
|
executor.execute(new Runnable() {
|
|
public void run() {
|
|
callback.showMessage("BLUETOOTH_INSTALL_LIBS");
|
|
}
|
|
});
|
|
}
|
|
throw new IOException(e.getMessage());
|
|
}
|
|
executor.execute(createContactSocketBinder());
|
|
}
|
|
|
|
@Override
|
|
public synchronized void stop() throws IOException {
|
|
super.stop();
|
|
if(socket != null) {
|
|
socket.close();
|
|
socket = null;
|
|
}
|
|
}
|
|
|
|
private Runnable createContactSocketBinder() {
|
|
return new Runnable() {
|
|
public void run() {
|
|
bindContactSocket();
|
|
}
|
|
};
|
|
}
|
|
|
|
private void bindContactSocket() {
|
|
String uuid;
|
|
synchronized(this) {
|
|
if(!started) return;
|
|
uuid = getUuid();
|
|
makeDeviceDiscoverable();
|
|
}
|
|
// Bind the socket
|
|
String url = "btspp://localhost:" + uuid + ";name=RFCOMM";
|
|
StreamConnectionNotifier scn;
|
|
try {
|
|
scn = (StreamConnectionNotifier) Connector.open(url);
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
return;
|
|
}
|
|
synchronized(this) {
|
|
if(!started) {
|
|
try {
|
|
scn.close();
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(Level.WARNING))
|
|
LOG.warning(e.getMessage());
|
|
}
|
|
return;
|
|
}
|
|
socket = scn;
|
|
setLocalBluetoothAddress(localDevice.getBluetoothAddress());
|
|
}
|
|
startContactAccepterThread();
|
|
}
|
|
|
|
private synchronized String getUuid() {
|
|
assert started;
|
|
TransportConfig c = callback.getConfig();
|
|
String uuid = c.get("uuid");
|
|
if(uuid == null) {
|
|
// Generate a (weakly) random UUID and store it
|
|
byte[] b = new byte[16];
|
|
new Random().nextBytes(b);
|
|
uuid = StringUtils.toHexString(b);
|
|
c.put("uuid", uuid);
|
|
callback.setConfig(c);
|
|
}
|
|
return uuid;
|
|
}
|
|
|
|
private synchronized void makeDeviceDiscoverable() {
|
|
assert started;
|
|
// Try to make the device discoverable (requires root on Linux)
|
|
try {
|
|
localDevice.setDiscoverable(DiscoveryAgent.GIAC);
|
|
} catch(BluetoothStateException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
}
|
|
}
|
|
|
|
private synchronized void setLocalBluetoothAddress(String address) {
|
|
assert started;
|
|
TransportProperties p = callback.getLocalProperties();
|
|
p.put("address", address);
|
|
callback.setLocalProperties(p);
|
|
}
|
|
|
|
private void startContactAccepterThread() {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
acceptContactConnections();
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
private void acceptContactConnections() {
|
|
while(true) {
|
|
StreamConnectionNotifier scn;
|
|
StreamConnection s;
|
|
synchronized(this) {
|
|
if(!started) return;
|
|
scn = socket;
|
|
}
|
|
try {
|
|
s = scn.acceptAndOpen();
|
|
} catch(IOException e) {
|
|
// This is expected when the socket is closed
|
|
if(LOG.isLoggable(Level.INFO)) LOG.info(e.getMessage());
|
|
return;
|
|
}
|
|
callback.incomingConnectionCreated(
|
|
new BluetoothTransportConnection(s));
|
|
}
|
|
}
|
|
|
|
public boolean shouldPoll() {
|
|
return true;
|
|
}
|
|
|
|
public long getPollingInterval() {
|
|
return pollingInterval;
|
|
}
|
|
|
|
public synchronized void poll() {
|
|
if(!started) return;
|
|
executor.execute(createConnectors());
|
|
}
|
|
|
|
private Runnable createConnectors() {
|
|
return new Runnable() {
|
|
public void run() {
|
|
connectAndCallBack();
|
|
}
|
|
};
|
|
}
|
|
|
|
private void connectAndCallBack() {
|
|
Map<ContactId, String> discovered = discoverContactUrls();
|
|
for(Entry<ContactId, String> e : discovered.entrySet()) {
|
|
ContactId c = e.getKey();
|
|
String url = e.getValue();
|
|
StreamTransportConnection s = connect(c, url);
|
|
if(s != null) callback.outgoingConnectionCreated(c, s);
|
|
}
|
|
}
|
|
|
|
private Map<ContactId, String> discoverContactUrls() {
|
|
DiscoveryAgent discoveryAgent;
|
|
Map<ContactId, TransportProperties> remote;
|
|
synchronized(this) {
|
|
if(!started) return Collections.emptyMap();
|
|
discoveryAgent = localDevice.getDiscoveryAgent();
|
|
remote = callback.getRemoteProperties();
|
|
}
|
|
Map<String, ContactId> addresses = new HashMap<String, ContactId>();
|
|
Map<ContactId, String> uuids = new HashMap<ContactId, String>();
|
|
for(Entry<ContactId, TransportProperties> 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) {
|
|
if(LOG.isLoggable(Level.FINE))
|
|
LOG.fine("Searching for " + uuid + " at " + address);
|
|
addresses.put(address, c);
|
|
uuids.put(c, uuid);
|
|
}
|
|
}
|
|
ContactListener listener = new ContactListener(discoveryAgent,
|
|
addresses, uuids);
|
|
synchronized(discoveryLock) {
|
|
try {
|
|
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener);
|
|
return listener.waitForUrls();
|
|
} catch(BluetoothStateException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
return Collections.emptyMap();
|
|
}
|
|
}
|
|
}
|
|
|
|
private StreamTransportConnection connect(ContactId c, String url) {
|
|
synchronized(this) {
|
|
if(!started) return null;
|
|
}
|
|
try {
|
|
if(LOG.isLoggable(Level.INFO)) LOG.info("Connecting to " + url);
|
|
StreamConnection s = (StreamConnection) Connector.open(url);
|
|
if(LOG.isLoggable(Level.INFO)) LOG.info("Connected");
|
|
return new BluetoothTransportConnection(s);
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public StreamTransportConnection createConnection(ContactId c) {
|
|
String url = discoverContactUrls().get(c);
|
|
return url == null ? null : connect(c, url);
|
|
}
|
|
|
|
public StreamTransportConnection sendInvitation(int code, long timeout) {
|
|
return createInvitationConnection(code, timeout);
|
|
}
|
|
|
|
public StreamTransportConnection acceptInvitation(int code, long timeout) {
|
|
return createInvitationConnection(code, timeout);
|
|
}
|
|
|
|
private StreamTransportConnection createInvitationConnection(int code,
|
|
long timeout) {
|
|
// The invitee's device may not be discoverable, so both parties must
|
|
// try to initiate connections
|
|
String uuid = convertInvitationCodeToUuid(code);
|
|
ConnectionCallback c = new ConnectionCallback(uuid, timeout);
|
|
startOutgoingInvitationThread(c);
|
|
startIncomingInvitationThread(c);
|
|
StreamConnection s = c.waitForConnection();
|
|
return s == null ? null : new BluetoothTransportConnection(s);
|
|
}
|
|
|
|
private String convertInvitationCodeToUuid(int code) {
|
|
byte[] b = new byte[16];
|
|
new Random(code).nextBytes(b);
|
|
return StringUtils.toHexString(b);
|
|
}
|
|
|
|
private void startOutgoingInvitationThread(final ConnectionCallback c) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
createInvitationConnection(c);
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
private void createInvitationConnection(ConnectionCallback c) {
|
|
DiscoveryAgent discoveryAgent;
|
|
synchronized(this) {
|
|
if(!started) return;
|
|
discoveryAgent = localDevice.getDiscoveryAgent();
|
|
}
|
|
// Try to discover the other party until the invitation times out
|
|
long end = System.currentTimeMillis() + c.getTimeout();
|
|
String url = null;
|
|
while(url == null && System.currentTimeMillis() < end) {
|
|
InvitationListener listener = new InvitationListener(discoveryAgent,
|
|
c.getUuid());
|
|
synchronized(discoveryLock) {
|
|
try {
|
|
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener);
|
|
url = listener.waitForUrl();
|
|
} catch(BluetoothStateException e) {
|
|
if(LOG.isLoggable(Level.WARNING))
|
|
LOG.warning(e.getMessage());
|
|
return;
|
|
}
|
|
}
|
|
synchronized(this) {
|
|
if(!started) 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.getMessage());
|
|
}
|
|
}
|
|
|
|
private void startIncomingInvitationThread(final ConnectionCallback c) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
bindInvitationSocket(c);
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
private void bindInvitationSocket(ConnectionCallback c) {
|
|
synchronized(this) {
|
|
if(!started) return;
|
|
makeDeviceDiscoverable();
|
|
}
|
|
// Bind the socket
|
|
String url = "btspp://localhost:" + c.getUuid() + ";name=RFCOMM";
|
|
StreamConnectionNotifier scn;
|
|
try {
|
|
scn = (StreamConnectionNotifier) Connector.open(url);
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
return;
|
|
}
|
|
startInvitationAccepterThread(c, scn);
|
|
// Close the socket when the invitation times out
|
|
try {
|
|
Thread.sleep(c.getTimeout());
|
|
scn.close();
|
|
} catch(InterruptedException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
} catch(IOException e) {
|
|
if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage());
|
|
}
|
|
}
|
|
|
|
private void startInvitationAccepterThread(final ConnectionCallback c,
|
|
final StreamConnectionNotifier scn) {
|
|
new Thread() {
|
|
@Override
|
|
public void run() {
|
|
acceptInvitationConnection(c, scn);
|
|
}
|
|
}.start();
|
|
}
|
|
|
|
private void acceptInvitationConnection(ConnectionCallback c,
|
|
StreamConnectionNotifier scn) {
|
|
synchronized(this) {
|
|
if(!started) return;
|
|
}
|
|
try {
|
|
StreamConnection s = scn.acceptAndOpen();
|
|
c.addConnection(s);
|
|
} catch(IOException e) {
|
|
// This is expected when the socket is closed
|
|
if(LOG.isLoggable(Level.INFO)) LOG.info(e.getMessage());
|
|
}
|
|
}
|
|
}
|