mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-16 20:59:54 +01:00
Implement backend for connect via bluetooth
This commit is contained in:
@@ -59,8 +59,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class AndroidBluetoothPlugin
|
class AndroidBluetoothPlugin extends
|
||||||
extends BluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
|
AbstractBluetoothPlugin<BluetoothSocket, BluetoothServerSocket> {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(AndroidBluetoothPlugin.class.getName());
|
getLogger(AndroidBluetoothPlugin.class.getName());
|
||||||
@@ -75,6 +75,7 @@ class AndroidBluetoothPlugin
|
|||||||
|
|
||||||
// Non-null if the plugin started successfully
|
// Non-null if the plugin started successfully
|
||||||
private volatile BluetoothAdapter adapter = null;
|
private volatile BluetoothAdapter adapter = null;
|
||||||
|
private volatile boolean stopDiscoverAndConnect;
|
||||||
|
|
||||||
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
||||||
BluetoothConnectionFactory<BluetoothSocket> connectionFactory,
|
BluetoothConnectionFactory<BluetoothSocket> connectionFactory,
|
||||||
@@ -187,22 +188,40 @@ class AndroidBluetoothPlugin
|
|||||||
@Nullable
|
@Nullable
|
||||||
DuplexTransportConnection discoverAndConnect(String uuid) {
|
DuplexTransportConnection discoverAndConnect(String uuid) {
|
||||||
if (adapter == null) return null;
|
if (adapter == null) return null;
|
||||||
for (String address : discoverDevices()) {
|
if (!discoverSemaphore.tryAcquire()) {
|
||||||
try {
|
LOG.info("Discover already running");
|
||||||
if (LOG.isLoggable(INFO))
|
return null;
|
||||||
LOG.info("Connecting to " + scrubMacAddress(address));
|
}
|
||||||
return connectTo(address, uuid);
|
try {
|
||||||
} catch (IOException e) {
|
stopDiscoverAndConnect = false;
|
||||||
if (LOG.isLoggable(INFO)) {
|
for (String address : discoverDevices()) {
|
||||||
LOG.info("Could not connect to "
|
if (stopDiscoverAndConnect) {
|
||||||
+ scrubMacAddress(address));
|
break;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connecting to " + scrubMacAddress(address));
|
||||||
|
return connectTo(address, uuid);
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Could not connect to "
|
||||||
|
+ scrubMacAddress(address));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
discoverSemaphore.release();
|
||||||
}
|
}
|
||||||
LOG.info("Could not connect to any devices");
|
LOG.info("Could not connect to any devices");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopDiscoverAndConnect() {
|
||||||
|
stopDiscoverAndConnect = true;
|
||||||
|
adapter.cancelDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
private Collection<String> discoverDevices() {
|
private Collection<String> discoverDevices() {
|
||||||
List<String> addresses = new ArrayList<>();
|
List<String> addresses = new ArrayList<>();
|
||||||
BlockingQueue<Intent> intents = new LinkedBlockingQueue<>();
|
BlockingQueue<Intent> intents = new LinkedBlockingQueue<>();
|
||||||
|
|||||||
@@ -0,0 +1,623 @@
|
|||||||
|
package org.briarproject.bramble.plugin.bluetooth;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.FormatException;
|
||||||
|
import org.briarproject.bramble.api.Multiset;
|
||||||
|
import org.briarproject.bramble.api.Pair;
|
||||||
|
import org.briarproject.bramble.api.data.BdfList;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
|
||||||
|
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
|
||||||
|
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.plugin.Backoff;
|
||||||
|
import org.briarproject.bramble.api.plugin.ConnectionHandler;
|
||||||
|
import org.briarproject.bramble.api.plugin.PluginCallback;
|
||||||
|
import org.briarproject.bramble.api.plugin.PluginException;
|
||||||
|
import org.briarproject.bramble.api.plugin.TransportId;
|
||||||
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
|
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||||
|
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
|
||||||
|
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
|
||||||
|
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
|
||||||
|
import org.briarproject.bramble.api.settings.Settings;
|
||||||
|
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.annotation.concurrent.GuardedBy;
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
|
||||||
|
import static java.util.logging.Level.INFO;
|
||||||
|
import static java.util.logging.Level.WARNING;
|
||||||
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
|
||||||
|
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
|
||||||
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.macToBytes;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.macToString;
|
||||||
|
|
||||||
|
@MethodsNotNullByDefault
|
||||||
|
@ParametersNotNullByDefault
|
||||||
|
abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
|
||||||
|
EventListener {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
getLogger(AbstractBluetoothPlugin.class.getName());
|
||||||
|
|
||||||
|
private final BluetoothConnectionLimiter connectionLimiter;
|
||||||
|
final BluetoothConnectionFactory<S> connectionFactory;
|
||||||
|
|
||||||
|
private final Executor ioExecutor, wakefulIoExecutor;
|
||||||
|
private final SecureRandom secureRandom;
|
||||||
|
private final Backoff backoff;
|
||||||
|
private final PluginCallback callback;
|
||||||
|
private final int maxLatency, maxIdleTime;
|
||||||
|
private final AtomicBoolean used = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean everConnected = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
protected final PluginState state = new PluginState();
|
||||||
|
protected final Semaphore discoverSemaphore = new Semaphore(1);
|
||||||
|
|
||||||
|
private volatile String contactConnectionsUuid = null;
|
||||||
|
|
||||||
|
abstract void initialiseAdapter() throws IOException;
|
||||||
|
|
||||||
|
abstract boolean isAdapterEnabled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local Bluetooth address, or null if no valid address can
|
||||||
|
* be found.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
abstract String getBluetoothAddress();
|
||||||
|
|
||||||
|
abstract SS openServerSocket(String uuid) throws IOException;
|
||||||
|
|
||||||
|
abstract void tryToClose(@Nullable SS ss);
|
||||||
|
|
||||||
|
abstract DuplexTransportConnection acceptConnection(SS ss)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
abstract boolean isValidAddress(String address);
|
||||||
|
|
||||||
|
abstract DuplexTransportConnection connectTo(String address, String uuid)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
abstract DuplexTransportConnection discoverAndConnect(String uuid);
|
||||||
|
|
||||||
|
AbstractBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
||||||
|
BluetoothConnectionFactory<S> connectionFactory,
|
||||||
|
Executor ioExecutor,
|
||||||
|
Executor wakefulIoExecutor,
|
||||||
|
SecureRandom secureRandom,
|
||||||
|
Backoff backoff,
|
||||||
|
PluginCallback callback,
|
||||||
|
int maxLatency,
|
||||||
|
int maxIdleTime) {
|
||||||
|
this.connectionLimiter = connectionLimiter;
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
this.ioExecutor = ioExecutor;
|
||||||
|
this.wakefulIoExecutor = wakefulIoExecutor;
|
||||||
|
this.secureRandom = secureRandom;
|
||||||
|
this.backoff = backoff;
|
||||||
|
this.callback = callback;
|
||||||
|
this.maxLatency = maxLatency;
|
||||||
|
this.maxIdleTime = maxIdleTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAdapterEnabled() {
|
||||||
|
LOG.info("Bluetooth enabled");
|
||||||
|
// We may not have been able to get the local address before
|
||||||
|
ioExecutor.execute(this::updateProperties);
|
||||||
|
if (getState() == INACTIVE) bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onAdapterDisabled() {
|
||||||
|
LOG.info("Bluetooth disabled");
|
||||||
|
connectionLimiter.allConnectionsClosed();
|
||||||
|
// The server socket may not have been closed automatically
|
||||||
|
SS ss = state.clearServerSocket();
|
||||||
|
if (ss != null) {
|
||||||
|
LOG.info("Closing server socket");
|
||||||
|
tryToClose(ss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransportId getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxLatency() {
|
||||||
|
return maxLatency;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getMaxIdleTime() {
|
||||||
|
return maxIdleTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start() throws PluginException {
|
||||||
|
if (used.getAndSet(true)) throw new IllegalStateException();
|
||||||
|
Settings settings = callback.getSettings();
|
||||||
|
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
||||||
|
DEFAULT_PREF_PLUGIN_ENABLE);
|
||||||
|
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
|
||||||
|
DEFAULT_PREF_EVER_CONNECTED));
|
||||||
|
state.setStarted(enabledByUser);
|
||||||
|
try {
|
||||||
|
initialiseAdapter();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new PluginException(e);
|
||||||
|
}
|
||||||
|
updateProperties();
|
||||||
|
if (enabledByUser && isAdapterEnabled()) bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bind() {
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
if (getState() != INACTIVE) return;
|
||||||
|
// Bind a server socket to accept connections from contacts
|
||||||
|
SS ss;
|
||||||
|
try {
|
||||||
|
ss = openServerSocket(contactConnectionsUuid);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!state.setServerSocket(ss)) {
|
||||||
|
LOG.info("Closing redundant server socket");
|
||||||
|
tryToClose(ss);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
backoff.reset();
|
||||||
|
acceptContactConnections(ss);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateProperties() {
|
||||||
|
TransportProperties p = callback.getLocalProperties();
|
||||||
|
String address = p.get(PROP_ADDRESS);
|
||||||
|
String uuid = p.get(PROP_UUID);
|
||||||
|
Settings s = callback.getSettings();
|
||||||
|
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
|
||||||
|
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
|
||||||
|
boolean changed = false;
|
||||||
|
if (address == null || isReflected) {
|
||||||
|
address = getBluetoothAddress();
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Local address " + scrubMacAddress(address));
|
||||||
|
}
|
||||||
|
if (address == null) {
|
||||||
|
if (everConnected.get()) {
|
||||||
|
address = getReflectedAddress();
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Reflected address " +
|
||||||
|
scrubMacAddress(address));
|
||||||
|
}
|
||||||
|
if (address != null) {
|
||||||
|
changed = true;
|
||||||
|
isReflected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changed = true;
|
||||||
|
isReflected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uuid == null) {
|
||||||
|
byte[] random = new byte[UUID_BYTES];
|
||||||
|
secureRandom.nextBytes(random);
|
||||||
|
uuid = UUID.nameUUIDFromBytes(random).toString();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
contactConnectionsUuid = uuid;
|
||||||
|
if (changed) {
|
||||||
|
p = new TransportProperties();
|
||||||
|
// If we previously used a reflected address and there's no longer
|
||||||
|
// a reflected address with enough votes to be used, we'll continue
|
||||||
|
// to use the old reflected address until there's a new winner
|
||||||
|
if (address != null) p.put(PROP_ADDRESS, address);
|
||||||
|
p.put(PROP_UUID, uuid);
|
||||||
|
callback.mergeLocalProperties(p);
|
||||||
|
s = new Settings();
|
||||||
|
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
|
||||||
|
callback.mergeSettings(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private String getReflectedAddress() {
|
||||||
|
// Count the number of votes for each reflected address
|
||||||
|
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
|
||||||
|
Multiset<String> votes = new Multiset<>();
|
||||||
|
for (TransportProperties p : callback.getRemoteProperties()) {
|
||||||
|
String address = p.get(key);
|
||||||
|
if (address != null && isValidAddress(address)) votes.add(address);
|
||||||
|
}
|
||||||
|
// If an address gets more than half of the votes, accept it
|
||||||
|
int total = votes.getTotal();
|
||||||
|
for (String address : votes.keySet()) {
|
||||||
|
if (votes.getCount(address) * 2 > total) return address;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptContactConnections(SS ss) {
|
||||||
|
while (true) {
|
||||||
|
DuplexTransportConnection conn;
|
||||||
|
try {
|
||||||
|
conn = acceptConnection(ss);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This is expected when the server socket is closed
|
||||||
|
LOG.info("Server socket closed");
|
||||||
|
state.clearServerSocket();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG.info("Connection received");
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
backoff.reset();
|
||||||
|
setEverConnected();
|
||||||
|
callback.handleConnection(conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setEverConnected() {
|
||||||
|
if (!everConnected.getAndSet(true)) {
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
Settings s = new Settings();
|
||||||
|
s.putBoolean(PREF_EVER_CONNECTED, true);
|
||||||
|
callback.mergeSettings(s);
|
||||||
|
// Contacts may already have sent a reflected address
|
||||||
|
updateProperties();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
SS ss = state.setStopped();
|
||||||
|
tryToClose(ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public State getState() {
|
||||||
|
return state.getState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getReasonsDisabled() {
|
||||||
|
return state.getReasonsDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldPoll() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getPollingInterval() {
|
||||||
|
return backoff.getPollingInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
|
||||||
|
properties) {
|
||||||
|
if (getState() != ACTIVE) return;
|
||||||
|
backoff.increment();
|
||||||
|
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
|
||||||
|
connect(p.getFirst(), p.getSecond());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect(TransportProperties p, ConnectionHandler h) {
|
||||||
|
String address = p.get(PROP_ADDRESS);
|
||||||
|
if (isNullOrEmpty(address)) return;
|
||||||
|
String uuid = p.get(PROP_UUID);
|
||||||
|
if (isNullOrEmpty(uuid)) return;
|
||||||
|
wakefulIoExecutor.execute(() -> {
|
||||||
|
DuplexTransportConnection d = createConnection(p);
|
||||||
|
if (d != null) {
|
||||||
|
backoff.reset();
|
||||||
|
setEverConnected();
|
||||||
|
h.handleConnection(d);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private DuplexTransportConnection connect(String address, String uuid) {
|
||||||
|
// Validate the address
|
||||||
|
if (!isValidAddress(address)) {
|
||||||
|
if (LOG.isLoggable(WARNING))
|
||||||
|
// Not scrubbing here to be able to figure out the problem
|
||||||
|
LOG.warning("Invalid address " + address);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Validate the UUID
|
||||||
|
try {
|
||||||
|
//noinspection ResultOfMethodCallIgnored
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connecting to " + scrubMacAddress(address));
|
||||||
|
try {
|
||||||
|
DuplexTransportConnection conn = connectTo(address, uuid);
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connected to " + scrubMacAddress(address));
|
||||||
|
return conn;
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Could not connect to " + scrubMacAddress(address));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DuplexTransportConnection createConnection(TransportProperties p) {
|
||||||
|
if (getState() != ACTIVE) return null;
|
||||||
|
if (!connectionLimiter.canOpenContactConnection()) return null;
|
||||||
|
String address = p.get(PROP_ADDRESS);
|
||||||
|
if (isNullOrEmpty(address)) return null;
|
||||||
|
String uuid = p.get(PROP_UUID);
|
||||||
|
if (isNullOrEmpty(uuid)) return null;
|
||||||
|
DuplexTransportConnection conn = connect(address, uuid);
|
||||||
|
if (conn != null) connectionLimiter.connectionOpened(conn);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsKeyAgreement() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
|
||||||
|
if (getState() != ACTIVE) return null;
|
||||||
|
// No truncation necessary because COMMIT_LENGTH = 16
|
||||||
|
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
||||||
|
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
||||||
|
// Bind a server socket for receiving key agreement connections
|
||||||
|
SS ss;
|
||||||
|
try {
|
||||||
|
ss = openServerSocket(uuid);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (getState() != ACTIVE) {
|
||||||
|
tryToClose(ss);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
BdfList descriptor = new BdfList();
|
||||||
|
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
||||||
|
String address = getBluetoothAddress();
|
||||||
|
if (address != null) descriptor.add(macToBytes(address));
|
||||||
|
return new BluetoothKeyAgreementListener(descriptor, ss);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DuplexTransportConnection createKeyAgreementConnection(
|
||||||
|
byte[] commitment, BdfList descriptor) {
|
||||||
|
if (getState() != ACTIVE) return null;
|
||||||
|
// No truncation necessary because COMMIT_LENGTH = 16
|
||||||
|
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
||||||
|
DuplexTransportConnection conn;
|
||||||
|
if (descriptor.size() == 1) {
|
||||||
|
if (LOG.isLoggable(INFO)) {
|
||||||
|
LOG.info("Discovering address for key agreement UUID " +
|
||||||
|
uuid);
|
||||||
|
}
|
||||||
|
conn = discoverAndConnect(uuid);
|
||||||
|
} else {
|
||||||
|
String address;
|
||||||
|
try {
|
||||||
|
address = parseAddress(descriptor);
|
||||||
|
} catch (FormatException e) {
|
||||||
|
LOG.info("Invalid address in key agreement descriptor");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (LOG.isLoggable(INFO))
|
||||||
|
LOG.info("Connecting to key agreement UUID " + uuid);
|
||||||
|
conn = connect(address, uuid);
|
||||||
|
}
|
||||||
|
if (conn != null) {
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
setEverConnected();
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseAddress(BdfList descriptor) throws FormatException {
|
||||||
|
byte[] mac = descriptor.getRaw(1);
|
||||||
|
if (mac.length != 6) throw new FormatException();
|
||||||
|
return macToString(mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDiscovering() {
|
||||||
|
return discoverSemaphore.availablePermits() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DuplexTransportConnection discoverAndConnectForSetup(String uuid) {
|
||||||
|
DuplexTransportConnection conn = discoverAndConnect(uuid);
|
||||||
|
if (conn != null) {
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
setEverConnected();
|
||||||
|
}
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsRendezvous() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
|
||||||
|
boolean alice, ConnectionHandler incoming) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void eventOccurred(Event e) {
|
||||||
|
if (e instanceof SettingsUpdatedEvent) {
|
||||||
|
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
|
||||||
|
if (s.getNamespace().equals(ID.getString()))
|
||||||
|
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
|
||||||
|
} else if (e instanceof KeyAgreementListeningEvent) {
|
||||||
|
ioExecutor.execute(connectionLimiter::keyAgreementStarted);
|
||||||
|
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
|
||||||
|
ioExecutor.execute(connectionLimiter::keyAgreementEnded);
|
||||||
|
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
|
||||||
|
RemoteTransportPropertiesUpdatedEvent r =
|
||||||
|
(RemoteTransportPropertiesUpdatedEvent) e;
|
||||||
|
if (r.getTransportId().equals(ID)) {
|
||||||
|
ioExecutor.execute(this::updateProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IoExecutor
|
||||||
|
private void onSettingsUpdated(Settings settings) {
|
||||||
|
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
||||||
|
DEFAULT_PREF_PLUGIN_ENABLE);
|
||||||
|
SS ss = state.setEnabledByUser(enabledByUser);
|
||||||
|
State s = getState();
|
||||||
|
if (ss != null) {
|
||||||
|
LOG.info("Disabled by user, closing server socket");
|
||||||
|
tryToClose(ss);
|
||||||
|
} else if (s == INACTIVE) {
|
||||||
|
if (isAdapterEnabled()) {
|
||||||
|
LOG.info("Enabled by user, opening server socket");
|
||||||
|
bind();
|
||||||
|
} else {
|
||||||
|
LOG.info("Enabled by user but adapter is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
||||||
|
|
||||||
|
private final SS ss;
|
||||||
|
|
||||||
|
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
|
||||||
|
super(descriptor);
|
||||||
|
this.ss = ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public KeyAgreementConnection accept() throws IOException {
|
||||||
|
DuplexTransportConnection conn = acceptConnection(ss);
|
||||||
|
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
|
||||||
|
connectionLimiter.connectionOpened(conn);
|
||||||
|
return new KeyAgreementConnection(conn, ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
tryToClose(ss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ThreadSafe
|
||||||
|
@NotNullByDefault
|
||||||
|
private class PluginState {
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
private boolean started = false,
|
||||||
|
stopped = false,
|
||||||
|
enabledByUser = false;
|
||||||
|
|
||||||
|
@GuardedBy("this")
|
||||||
|
@Nullable
|
||||||
|
private SS serverSocket = null;
|
||||||
|
|
||||||
|
private synchronized void setStarted(boolean enabledByUser) {
|
||||||
|
started = true;
|
||||||
|
this.enabledByUser = enabledByUser;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private synchronized SS setStopped() {
|
||||||
|
stopped = true;
|
||||||
|
SS ss = serverSocket;
|
||||||
|
serverSocket = null;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private synchronized SS setEnabledByUser(boolean enabledByUser) {
|
||||||
|
this.enabledByUser = enabledByUser;
|
||||||
|
SS ss = null;
|
||||||
|
if (!enabledByUser) {
|
||||||
|
ss = serverSocket;
|
||||||
|
serverSocket = null;
|
||||||
|
}
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized boolean setServerSocket(SS ss) {
|
||||||
|
if (stopped || serverSocket != null) return false;
|
||||||
|
serverSocket = ss;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private synchronized SS clearServerSocket() {
|
||||||
|
SS ss = serverSocket;
|
||||||
|
serverSocket = null;
|
||||||
|
callback.pluginStateChanged(getState());
|
||||||
|
return ss;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized State getState() {
|
||||||
|
if (!started || stopped) return STARTING_STOPPING;
|
||||||
|
if (!enabledByUser) return DISABLED;
|
||||||
|
return serverSocket == null ? INACTIVE : ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized int getReasonsDisabled() {
|
||||||
|
return getState() == DISABLED ? REASON_USER : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,606 +1,18 @@
|
|||||||
package org.briarproject.bramble.plugin.bluetooth;
|
package org.briarproject.bramble.plugin.bluetooth;
|
||||||
|
|
||||||
import org.briarproject.bramble.api.FormatException;
|
|
||||||
import org.briarproject.bramble.api.Multiset;
|
|
||||||
import org.briarproject.bramble.api.Pair;
|
|
||||||
import org.briarproject.bramble.api.data.BdfList;
|
|
||||||
import org.briarproject.bramble.api.event.Event;
|
|
||||||
import org.briarproject.bramble.api.event.EventListener;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
|
|
||||||
import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStoppedListeningEvent;
|
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
|
||||||
import org.briarproject.bramble.api.plugin.Backoff;
|
|
||||||
import org.briarproject.bramble.api.plugin.ConnectionHandler;
|
|
||||||
import org.briarproject.bramble.api.plugin.PluginCallback;
|
|
||||||
import org.briarproject.bramble.api.plugin.PluginException;
|
|
||||||
import org.briarproject.bramble.api.plugin.TransportId;
|
|
||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
|
||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
|
||||||
import org.briarproject.bramble.api.properties.event.RemoteTransportPropertiesUpdatedEvent;
|
|
||||||
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
|
|
||||||
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
|
|
||||||
import org.briarproject.bramble.api.settings.Settings;
|
|
||||||
import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.Executor;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import javax.annotation.concurrent.GuardedBy;
|
|
||||||
import javax.annotation.concurrent.ThreadSafe;
|
|
||||||
|
|
||||||
import static java.util.logging.Level.INFO;
|
@NotNullByDefault
|
||||||
import static java.util.logging.Level.WARNING;
|
public interface BluetoothPlugin extends DuplexPlugin {
|
||||||
import static java.util.logging.Logger.getLogger;
|
|
||||||
import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_ADDRESS_IS_REFLECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_EVER_CONNECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.DEFAULT_PREF_PLUGIN_ENABLE;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_ADDRESS_IS_REFLECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_EVER_CONNECTED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_ADDRESS;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
|
|
||||||
import static org.briarproject.bramble.api.plugin.BluetoothConstants.UUID_BYTES;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
|
|
||||||
import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING;
|
|
||||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.REFLECTED_PROPERTY_PREFIX;
|
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
|
||||||
import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.macToBytes;
|
|
||||||
import static org.briarproject.bramble.util.StringUtils.macToString;
|
|
||||||
|
|
||||||
@MethodsNotNullByDefault
|
boolean isDiscovering();
|
||||||
@ParametersNotNullByDefault
|
|
||||||
abstract class BluetoothPlugin<S, SS> implements DuplexPlugin, EventListener {
|
|
||||||
|
|
||||||
private static final Logger LOG =
|
|
||||||
getLogger(BluetoothPlugin.class.getName());
|
|
||||||
|
|
||||||
final BluetoothConnectionLimiter connectionLimiter;
|
|
||||||
final BluetoothConnectionFactory<S> connectionFactory;
|
|
||||||
|
|
||||||
private final Executor ioExecutor, wakefulIoExecutor;
|
|
||||||
private final SecureRandom secureRandom;
|
|
||||||
private final Backoff backoff;
|
|
||||||
private final PluginCallback callback;
|
|
||||||
private final int maxLatency, maxIdleTime;
|
|
||||||
private final AtomicBoolean used = new AtomicBoolean(false);
|
|
||||||
private final AtomicBoolean everConnected = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
protected final PluginState state = new PluginState();
|
|
||||||
|
|
||||||
private volatile String contactConnectionsUuid = null;
|
|
||||||
|
|
||||||
abstract void initialiseAdapter() throws IOException;
|
|
||||||
|
|
||||||
abstract boolean isAdapterEnabled();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the local Bluetooth address, or null if no valid address can
|
|
||||||
* be found.
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
abstract String getBluetoothAddress();
|
|
||||||
|
|
||||||
abstract SS openServerSocket(String uuid) throws IOException;
|
|
||||||
|
|
||||||
abstract void tryToClose(@Nullable SS ss);
|
|
||||||
|
|
||||||
abstract DuplexTransportConnection acceptConnection(SS ss)
|
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
abstract boolean isValidAddress(String address);
|
|
||||||
|
|
||||||
abstract DuplexTransportConnection connectTo(String address, String uuid)
|
|
||||||
throws IOException;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
abstract DuplexTransportConnection discoverAndConnect(String uuid);
|
DuplexTransportConnection discoverAndConnectForSetup(String uuid);
|
||||||
|
|
||||||
BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
|
void stopDiscoverAndConnect();
|
||||||
BluetoothConnectionFactory<S> connectionFactory,
|
|
||||||
Executor ioExecutor,
|
|
||||||
Executor wakefulIoExecutor,
|
|
||||||
SecureRandom secureRandom,
|
|
||||||
Backoff backoff,
|
|
||||||
PluginCallback callback,
|
|
||||||
int maxLatency,
|
|
||||||
int maxIdleTime) {
|
|
||||||
this.connectionLimiter = connectionLimiter;
|
|
||||||
this.connectionFactory = connectionFactory;
|
|
||||||
this.ioExecutor = ioExecutor;
|
|
||||||
this.wakefulIoExecutor = wakefulIoExecutor;
|
|
||||||
this.secureRandom = secureRandom;
|
|
||||||
this.backoff = backoff;
|
|
||||||
this.callback = callback;
|
|
||||||
this.maxLatency = maxLatency;
|
|
||||||
this.maxIdleTime = maxIdleTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAdapterEnabled() {
|
|
||||||
LOG.info("Bluetooth enabled");
|
|
||||||
// We may not have been able to get the local address before
|
|
||||||
ioExecutor.execute(this::updateProperties);
|
|
||||||
if (getState() == INACTIVE) bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onAdapterDisabled() {
|
|
||||||
LOG.info("Bluetooth disabled");
|
|
||||||
connectionLimiter.allConnectionsClosed();
|
|
||||||
// The server socket may not have been closed automatically
|
|
||||||
SS ss = state.clearServerSocket();
|
|
||||||
if (ss != null) {
|
|
||||||
LOG.info("Closing server socket");
|
|
||||||
tryToClose(ss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TransportId getId() {
|
|
||||||
return ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMaxLatency() {
|
|
||||||
return maxLatency;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMaxIdleTime() {
|
|
||||||
return maxIdleTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() throws PluginException {
|
|
||||||
if (used.getAndSet(true)) throw new IllegalStateException();
|
|
||||||
Settings settings = callback.getSettings();
|
|
||||||
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
|
||||||
DEFAULT_PREF_PLUGIN_ENABLE);
|
|
||||||
everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
|
|
||||||
DEFAULT_PREF_EVER_CONNECTED));
|
|
||||||
state.setStarted(enabledByUser);
|
|
||||||
try {
|
|
||||||
initialiseAdapter();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new PluginException(e);
|
|
||||||
}
|
|
||||||
updateProperties();
|
|
||||||
if (enabledByUser && isAdapterEnabled()) bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void bind() {
|
|
||||||
ioExecutor.execute(() -> {
|
|
||||||
if (getState() != INACTIVE) return;
|
|
||||||
// Bind a server socket to accept connections from contacts
|
|
||||||
SS ss;
|
|
||||||
try {
|
|
||||||
ss = openServerSocket(contactConnectionsUuid);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!state.setServerSocket(ss)) {
|
|
||||||
LOG.info("Closing redundant server socket");
|
|
||||||
tryToClose(ss);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
backoff.reset();
|
|
||||||
acceptContactConnections(ss);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateProperties() {
|
|
||||||
TransportProperties p = callback.getLocalProperties();
|
|
||||||
String address = p.get(PROP_ADDRESS);
|
|
||||||
String uuid = p.get(PROP_UUID);
|
|
||||||
Settings s = callback.getSettings();
|
|
||||||
boolean isReflected = s.getBoolean(PREF_ADDRESS_IS_REFLECTED,
|
|
||||||
DEFAULT_PREF_ADDRESS_IS_REFLECTED);
|
|
||||||
boolean changed = false;
|
|
||||||
if (address == null || isReflected) {
|
|
||||||
address = getBluetoothAddress();
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Local address " + scrubMacAddress(address));
|
|
||||||
}
|
|
||||||
if (address == null) {
|
|
||||||
if (everConnected.get()) {
|
|
||||||
address = getReflectedAddress();
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Reflected address " +
|
|
||||||
scrubMacAddress(address));
|
|
||||||
}
|
|
||||||
if (address != null) {
|
|
||||||
changed = true;
|
|
||||||
isReflected = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
changed = true;
|
|
||||||
isReflected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (uuid == null) {
|
|
||||||
byte[] random = new byte[UUID_BYTES];
|
|
||||||
secureRandom.nextBytes(random);
|
|
||||||
uuid = UUID.nameUUIDFromBytes(random).toString();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
contactConnectionsUuid = uuid;
|
|
||||||
if (changed) {
|
|
||||||
p = new TransportProperties();
|
|
||||||
// If we previously used a reflected address and there's no longer
|
|
||||||
// a reflected address with enough votes to be used, we'll continue
|
|
||||||
// to use the old reflected address until there's a new winner
|
|
||||||
if (address != null) p.put(PROP_ADDRESS, address);
|
|
||||||
p.put(PROP_UUID, uuid);
|
|
||||||
callback.mergeLocalProperties(p);
|
|
||||||
s = new Settings();
|
|
||||||
s.putBoolean(PREF_ADDRESS_IS_REFLECTED, isReflected);
|
|
||||||
callback.mergeSettings(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getReflectedAddress() {
|
|
||||||
// Count the number of votes for each reflected address
|
|
||||||
String key = REFLECTED_PROPERTY_PREFIX + PROP_ADDRESS;
|
|
||||||
Multiset<String> votes = new Multiset<>();
|
|
||||||
for (TransportProperties p : callback.getRemoteProperties()) {
|
|
||||||
String address = p.get(key);
|
|
||||||
if (address != null && isValidAddress(address)) votes.add(address);
|
|
||||||
}
|
|
||||||
// If an address gets more than half of the votes, accept it
|
|
||||||
int total = votes.getTotal();
|
|
||||||
for (String address : votes.keySet()) {
|
|
||||||
if (votes.getCount(address) * 2 > total) return address;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void acceptContactConnections(SS ss) {
|
|
||||||
while (true) {
|
|
||||||
DuplexTransportConnection conn;
|
|
||||||
try {
|
|
||||||
conn = acceptConnection(ss);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// This is expected when the server socket is closed
|
|
||||||
LOG.info("Server socket closed");
|
|
||||||
state.clearServerSocket();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LOG.info("Connection received");
|
|
||||||
connectionLimiter.connectionOpened(conn);
|
|
||||||
backoff.reset();
|
|
||||||
setEverConnected();
|
|
||||||
callback.handleConnection(conn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setEverConnected() {
|
|
||||||
if (!everConnected.getAndSet(true)) {
|
|
||||||
ioExecutor.execute(() -> {
|
|
||||||
Settings s = new Settings();
|
|
||||||
s.putBoolean(PREF_EVER_CONNECTED, true);
|
|
||||||
callback.mergeSettings(s);
|
|
||||||
// Contacts may already have sent a reflected address
|
|
||||||
updateProperties();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stop() {
|
|
||||||
SS ss = state.setStopped();
|
|
||||||
tryToClose(ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public State getState() {
|
|
||||||
return state.getState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getReasonsDisabled() {
|
|
||||||
return state.getReasonsDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean shouldPoll() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getPollingInterval() {
|
|
||||||
return backoff.getPollingInterval();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void poll(Collection<Pair<TransportProperties, ConnectionHandler>>
|
|
||||||
properties) {
|
|
||||||
if (getState() != ACTIVE) return;
|
|
||||||
backoff.increment();
|
|
||||||
for (Pair<TransportProperties, ConnectionHandler> p : properties) {
|
|
||||||
connect(p.getFirst(), p.getSecond());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void connect(TransportProperties p, ConnectionHandler h) {
|
|
||||||
String address = p.get(PROP_ADDRESS);
|
|
||||||
if (isNullOrEmpty(address)) return;
|
|
||||||
String uuid = p.get(PROP_UUID);
|
|
||||||
if (isNullOrEmpty(uuid)) return;
|
|
||||||
wakefulIoExecutor.execute(() -> {
|
|
||||||
DuplexTransportConnection d = createConnection(p);
|
|
||||||
if (d != null) {
|
|
||||||
backoff.reset();
|
|
||||||
setEverConnected();
|
|
||||||
h.handleConnection(d);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private DuplexTransportConnection connect(String address, String uuid) {
|
|
||||||
// Validate the address
|
|
||||||
if (!isValidAddress(address)) {
|
|
||||||
if (LOG.isLoggable(WARNING))
|
|
||||||
// Not scrubbing here to be able to figure out the problem
|
|
||||||
LOG.warning("Invalid address " + address);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// Validate the UUID
|
|
||||||
try {
|
|
||||||
//noinspection ResultOfMethodCallIgnored
|
|
||||||
UUID.fromString(uuid);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
if (LOG.isLoggable(WARNING)) LOG.warning("Invalid UUID " + uuid);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Connecting to " + scrubMacAddress(address));
|
|
||||||
try {
|
|
||||||
DuplexTransportConnection conn = connectTo(address, uuid);
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Connected to " + scrubMacAddress(address));
|
|
||||||
return conn;
|
|
||||||
} catch (IOException e) {
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Could not connect to " + scrubMacAddress(address));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DuplexTransportConnection createConnection(TransportProperties p) {
|
|
||||||
if (getState() != ACTIVE) return null;
|
|
||||||
if (!connectionLimiter.canOpenContactConnection()) return null;
|
|
||||||
String address = p.get(PROP_ADDRESS);
|
|
||||||
if (isNullOrEmpty(address)) return null;
|
|
||||||
String uuid = p.get(PROP_UUID);
|
|
||||||
if (isNullOrEmpty(uuid)) return null;
|
|
||||||
DuplexTransportConnection conn = connect(address, uuid);
|
|
||||||
if (conn != null) connectionLimiter.connectionOpened(conn);
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsKeyAgreement() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
|
|
||||||
if (getState() != ACTIVE) return null;
|
|
||||||
// No truncation necessary because COMMIT_LENGTH = 16
|
|
||||||
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
|
||||||
if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
|
|
||||||
// Bind a server socket for receiving key agreement connections
|
|
||||||
SS ss;
|
|
||||||
try {
|
|
||||||
ss = openServerSocket(uuid);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logException(LOG, WARNING, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (getState() != ACTIVE) {
|
|
||||||
tryToClose(ss);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
BdfList descriptor = new BdfList();
|
|
||||||
descriptor.add(TRANSPORT_ID_BLUETOOTH);
|
|
||||||
String address = getBluetoothAddress();
|
|
||||||
if (address != null) descriptor.add(macToBytes(address));
|
|
||||||
return new BluetoothKeyAgreementListener(descriptor, ss);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DuplexTransportConnection createKeyAgreementConnection(
|
|
||||||
byte[] commitment, BdfList descriptor) {
|
|
||||||
if (getState() != ACTIVE) return null;
|
|
||||||
// No truncation necessary because COMMIT_LENGTH = 16
|
|
||||||
String uuid = UUID.nameUUIDFromBytes(commitment).toString();
|
|
||||||
DuplexTransportConnection conn;
|
|
||||||
if (descriptor.size() == 1) {
|
|
||||||
if (LOG.isLoggable(INFO)) {
|
|
||||||
LOG.info("Discovering address for key agreement UUID " +
|
|
||||||
uuid);
|
|
||||||
}
|
|
||||||
conn = discoverAndConnect(uuid);
|
|
||||||
} else {
|
|
||||||
String address;
|
|
||||||
try {
|
|
||||||
address = parseAddress(descriptor);
|
|
||||||
} catch (FormatException e) {
|
|
||||||
LOG.info("Invalid address in key agreement descriptor");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (LOG.isLoggable(INFO))
|
|
||||||
LOG.info("Connecting to key agreement UUID " + uuid);
|
|
||||||
conn = connect(address, uuid);
|
|
||||||
}
|
|
||||||
if (conn != null) {
|
|
||||||
connectionLimiter.connectionOpened(conn);
|
|
||||||
setEverConnected();
|
|
||||||
}
|
|
||||||
return conn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseAddress(BdfList descriptor) throws FormatException {
|
|
||||||
byte[] mac = descriptor.getRaw(1);
|
|
||||||
if (mac.length != 6) throw new FormatException();
|
|
||||||
return macToString(mac);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsRendezvous() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
|
|
||||||
boolean alice, ConnectionHandler incoming) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void eventOccurred(Event e) {
|
|
||||||
if (e instanceof SettingsUpdatedEvent) {
|
|
||||||
SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
|
|
||||||
if (s.getNamespace().equals(ID.getString()))
|
|
||||||
ioExecutor.execute(() -> onSettingsUpdated(s.getSettings()));
|
|
||||||
} else if (e instanceof KeyAgreementListeningEvent) {
|
|
||||||
ioExecutor.execute(connectionLimiter::keyAgreementStarted);
|
|
||||||
} else if (e instanceof KeyAgreementStoppedListeningEvent) {
|
|
||||||
ioExecutor.execute(connectionLimiter::keyAgreementEnded);
|
|
||||||
} else if (e instanceof RemoteTransportPropertiesUpdatedEvent) {
|
|
||||||
RemoteTransportPropertiesUpdatedEvent r =
|
|
||||||
(RemoteTransportPropertiesUpdatedEvent) e;
|
|
||||||
if (r.getTransportId().equals(ID)) {
|
|
||||||
ioExecutor.execute(this::updateProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IoExecutor
|
|
||||||
private void onSettingsUpdated(Settings settings) {
|
|
||||||
boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE,
|
|
||||||
DEFAULT_PREF_PLUGIN_ENABLE);
|
|
||||||
SS ss = state.setEnabledByUser(enabledByUser);
|
|
||||||
State s = getState();
|
|
||||||
if (ss != null) {
|
|
||||||
LOG.info("Disabled by user, closing server socket");
|
|
||||||
tryToClose(ss);
|
|
||||||
} else if (s == INACTIVE) {
|
|
||||||
if (isAdapterEnabled()) {
|
|
||||||
LOG.info("Enabled by user, opening server socket");
|
|
||||||
bind();
|
|
||||||
} else {
|
|
||||||
LOG.info("Enabled by user but adapter is disabled");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class BluetoothKeyAgreementListener extends KeyAgreementListener {
|
|
||||||
|
|
||||||
private final SS ss;
|
|
||||||
|
|
||||||
private BluetoothKeyAgreementListener(BdfList descriptor, SS ss) {
|
|
||||||
super(descriptor);
|
|
||||||
this.ss = ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyAgreementConnection accept() throws IOException {
|
|
||||||
DuplexTransportConnection conn = acceptConnection(ss);
|
|
||||||
if (LOG.isLoggable(INFO)) LOG.info(ID + ": Incoming connection");
|
|
||||||
connectionLimiter.connectionOpened(conn);
|
|
||||||
return new KeyAgreementConnection(conn, ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
tryToClose(ss);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThreadSafe
|
|
||||||
@NotNullByDefault
|
|
||||||
protected class PluginState {
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
private boolean started = false,
|
|
||||||
stopped = false,
|
|
||||||
enabledByUser = false;
|
|
||||||
|
|
||||||
@GuardedBy("this")
|
|
||||||
@Nullable
|
|
||||||
private SS serverSocket = null;
|
|
||||||
|
|
||||||
synchronized void setStarted(boolean enabledByUser) {
|
|
||||||
started = true;
|
|
||||||
this.enabledByUser = enabledByUser;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
synchronized SS setStopped() {
|
|
||||||
stopped = true;
|
|
||||||
SS ss = serverSocket;
|
|
||||||
serverSocket = null;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
synchronized SS setEnabledByUser(boolean enabledByUser) {
|
|
||||||
this.enabledByUser = enabledByUser;
|
|
||||||
SS ss = null;
|
|
||||||
if (!enabledByUser) {
|
|
||||||
ss = serverSocket;
|
|
||||||
serverSocket = null;
|
|
||||||
}
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized boolean setServerSocket(SS ss) {
|
|
||||||
if (stopped || serverSocket != null) return false;
|
|
||||||
serverSocket = ss;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
synchronized SS clearServerSocket() {
|
|
||||||
SS ss = serverSocket;
|
|
||||||
serverSocket = null;
|
|
||||||
callback.pluginStateChanged(getState());
|
|
||||||
return ss;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized State getState() {
|
|
||||||
if (!started || stopped) return STARTING_STOPPING;
|
|
||||||
if (!enabledByUser) return DISABLED;
|
|
||||||
return serverSocket == null ? INACTIVE : ACTIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized int getReasonsDisabled() {
|
|
||||||
return getState() == DISABLED ? REASON_USER : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import static org.briarproject.bramble.util.StringUtils.isValidMac;
|
|||||||
|
|
||||||
@MethodsNotNullByDefault
|
@MethodsNotNullByDefault
|
||||||
@ParametersNotNullByDefault
|
@ParametersNotNullByDefault
|
||||||
class JavaBluetoothPlugin
|
class JavaBluetoothPlugin extends
|
||||||
extends BluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
|
AbstractBluetoothPlugin<StreamConnection, StreamConnectionNotifier> {
|
||||||
|
|
||||||
private static final Logger LOG =
|
private static final Logger LOG =
|
||||||
getLogger(JavaBluetoothPlugin.class.getName());
|
getLogger(JavaBluetoothPlugin.class.getName());
|
||||||
@@ -108,6 +108,11 @@ class JavaBluetoothPlugin
|
|||||||
return null; // TODO
|
return null; // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopDiscoverAndConnect() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
private String makeUrl(String address, String uuid) {
|
private String makeUrl(String address, String uuid) {
|
||||||
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public class AddNearbyContactIntroFragment extends BaseFragment {
|
|||||||
scrollView = v.findViewById(R.id.scrollView);
|
scrollView = v.findViewById(R.id.scrollView);
|
||||||
View button = v.findViewById(R.id.continueButton);
|
View button = v.findViewById(R.id.continueButton);
|
||||||
button.setOnClickListener(view -> {
|
button.setOnClickListener(view -> {
|
||||||
|
viewModel.stopDiscovery();
|
||||||
viewModel.onContinueClicked();
|
viewModel.onContinueClicked();
|
||||||
if (permissionManager.checkPermissions()) {
|
if (permissionManager.checkPermissions()) {
|
||||||
viewModel.showQrCodeFragmentIfAllowed();
|
viewModel.showQrCodeFragmentIfAllowed();
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import org.briarproject.bramble.api.plugin.TransportId;
|
|||||||
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
|
import org.briarproject.bramble.api.plugin.event.TransportStateEvent;
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||||
|
import org.briarproject.bramble.plugin.bluetooth.BluetoothPlugin;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeFinished;
|
import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeFinished;
|
||||||
import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult.Error;
|
import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult.Error;
|
||||||
@@ -149,7 +150,9 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
|||||||
@Nullable
|
@Nullable
|
||||||
private final BluetoothAdapter bt;
|
private final BluetoothAdapter bt;
|
||||||
@Nullable // UiThread
|
@Nullable // UiThread
|
||||||
private Plugin wifiPlugin, bluetoothPlugin;
|
private Plugin wifiPlugin;
|
||||||
|
@Nullable // UiThread
|
||||||
|
private BluetoothPlugin bluetoothPlugin;
|
||||||
|
|
||||||
// UiThread
|
// UiThread
|
||||||
private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
|
private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN;
|
||||||
@@ -195,7 +198,8 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
|||||||
this.connectionManager = connectionManager;
|
this.connectionManager = connectionManager;
|
||||||
bt = BluetoothAdapter.getDefaultAdapter();
|
bt = BluetoothAdapter.getDefaultAdapter();
|
||||||
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
|
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
|
||||||
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
bluetoothPlugin = (BluetoothPlugin) pluginManager
|
||||||
|
.getPlugin(BluetoothConstants.ID);
|
||||||
qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
|
qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this);
|
||||||
eventBus.addListener(this);
|
eventBus.addListener(this);
|
||||||
IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
|
IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED);
|
||||||
@@ -218,7 +222,8 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
|||||||
@UiThread
|
@UiThread
|
||||||
void resetPlugins() {
|
void resetPlugins() {
|
||||||
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
|
wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID);
|
||||||
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
bluetoothPlugin = (BluetoothPlugin) pluginManager
|
||||||
|
.getPlugin(BluetoothConstants.ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@@ -375,6 +380,13 @@ class AddNearbyContactViewModel extends AndroidViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void stopDiscovery() {
|
||||||
|
if (!isBluetoothSupported() || !bluetoothPlugin.isDiscovering()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bluetoothPlugin.stopDiscoverAndConnect();
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("StatementWithEmptyBody")
|
@SuppressWarnings("StatementWithEmptyBody")
|
||||||
@UiThread
|
@UiThread
|
||||||
void showQrCodeFragmentIfAllowed() {
|
void showQrCodeFragmentIfAllowed() {
|
||||||
|
|||||||
@@ -6,23 +6,30 @@ import android.bluetooth.BluetoothAdapter;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||||
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
import org.briarproject.bramble.api.connection.ConnectionRegistry;
|
||||||
import org.briarproject.bramble.api.contact.ContactId;
|
import org.briarproject.bramble.api.contact.ContactId;
|
||||||
|
import org.briarproject.bramble.api.db.DbException;
|
||||||
|
import org.briarproject.bramble.api.event.Event;
|
||||||
|
import org.briarproject.bramble.api.event.EventBus;
|
||||||
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
import org.briarproject.bramble.api.lifecycle.IoExecutor;
|
||||||
import org.briarproject.bramble.api.plugin.BluetoothConstants;
|
|
||||||
import org.briarproject.bramble.api.plugin.Plugin;
|
|
||||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||||
|
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
|
||||||
|
import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent;
|
||||||
|
import org.briarproject.bramble.api.properties.TransportPropertyManager;
|
||||||
import org.briarproject.bramble.api.system.AndroidExecutor;
|
import org.briarproject.bramble.api.system.AndroidExecutor;
|
||||||
|
import org.briarproject.bramble.plugin.bluetooth.BluetoothPlugin;
|
||||||
import org.briarproject.briar.R;
|
import org.briarproject.briar.R;
|
||||||
import org.briarproject.briar.android.contact.ContactItem;
|
import org.briarproject.briar.android.contact.ContactItem;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.annotation.UiThread;
|
import androidx.annotation.UiThread;
|
||||||
@@ -31,17 +38,24 @@ import androidx.appcompat.app.AlertDialog;
|
|||||||
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
import static android.Manifest.permission.ACCESS_FINE_LOCATION;
|
||||||
import static android.os.Build.VERSION.SDK_INT;
|
import static android.os.Build.VERSION.SDK_INT;
|
||||||
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
import static java.util.logging.Level.WARNING;
|
import static java.util.logging.Level.WARNING;
|
||||||
import static java.util.logging.Logger.getLogger;
|
import static java.util.logging.Logger.getLogger;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.ID;
|
||||||
|
import static org.briarproject.bramble.api.plugin.BluetoothConstants.PROP_UUID;
|
||||||
|
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
|
||||||
import static org.briarproject.bramble.util.LogUtils.logException;
|
import static org.briarproject.bramble.util.LogUtils.logException;
|
||||||
|
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
|
import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
|
||||||
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
|
import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
|
||||||
|
|
||||||
class BluetoothConnecter {
|
class BluetoothConnecter implements EventListener {
|
||||||
|
|
||||||
private final Logger LOG = getLogger(BluetoothConnecter.class.getName());
|
private final Logger LOG = getLogger(BluetoothConnecter.class.getName());
|
||||||
|
|
||||||
|
private final long BT_ACTIVE_TIMEOUT = SECONDS.toMillis(5);
|
||||||
|
|
||||||
private enum Permission {
|
private enum Permission {
|
||||||
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
|
||||||
}
|
}
|
||||||
@@ -52,32 +66,41 @@ class BluetoothConnecter {
|
|||||||
private final AndroidExecutor androidExecutor;
|
private final AndroidExecutor androidExecutor;
|
||||||
private final ConnectionRegistry connectionRegistry;
|
private final ConnectionRegistry connectionRegistry;
|
||||||
private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
private final BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
private final EventBus eventBus;
|
||||||
|
private final TransportPropertyManager transportPropertyManager;
|
||||||
|
private final ConnectionManager connectionManager;
|
||||||
|
|
||||||
private volatile Plugin bluetoothPlugin;
|
private volatile BluetoothPlugin bluetoothPlugin;
|
||||||
|
|
||||||
private Permission locationPermission = Permission.UNKNOWN;
|
private Permission locationPermission = Permission.UNKNOWN;
|
||||||
|
private ContactId contactId = null;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
BluetoothConnecter(Application app,
|
BluetoothConnecter(Application app,
|
||||||
PluginManager pluginManager,
|
PluginManager pluginManager,
|
||||||
@IoExecutor Executor ioExecutor,
|
@IoExecutor Executor ioExecutor,
|
||||||
AndroidExecutor androidExecutor,
|
AndroidExecutor androidExecutor,
|
||||||
ConnectionRegistry connectionRegistry) {
|
ConnectionRegistry connectionRegistry,
|
||||||
|
EventBus eventBus,
|
||||||
|
TransportPropertyManager transportPropertyManager,
|
||||||
|
ConnectionManager connectionManager) {
|
||||||
this.app = app;
|
this.app = app;
|
||||||
this.pluginManager = pluginManager;
|
this.pluginManager = pluginManager;
|
||||||
this.ioExecutor = ioExecutor;
|
this.ioExecutor = ioExecutor;
|
||||||
this.androidExecutor = androidExecutor;
|
this.androidExecutor = androidExecutor;
|
||||||
this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
this.bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID);
|
||||||
this.connectionRegistry = connectionRegistry;
|
this.connectionRegistry = connectionRegistry;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.transportPropertyManager = transportPropertyManager;
|
||||||
|
this.connectionManager = connectionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isConnectedViaBluetooth(ContactId contactId) {
|
boolean isConnectedViaBluetooth(ContactId contactId) {
|
||||||
return connectionRegistry.isConnected(contactId, BluetoothConstants.ID);
|
return connectionRegistry.isConnected(contactId, ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isDiscovering() {
|
boolean isDiscovering() {
|
||||||
// TODO bluetoothPlugin.isDiscovering()
|
return bluetoothPlugin.isDiscovering();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +112,7 @@ class BluetoothConnecter {
|
|||||||
// When this class is instantiated before we are logged in
|
// When this class is instantiated before we are logged in
|
||||||
// (like when returning to a killed activity), bluetoothPlugin would be
|
// (like when returning to a killed activity), bluetoothPlugin would be
|
||||||
// null and we consider bluetooth not supported. So reset here.
|
// null and we consider bluetooth not supported. So reset here.
|
||||||
bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID);
|
bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
@@ -149,30 +172,84 @@ class BluetoothConnecter {
|
|||||||
|
|
||||||
@UiThread
|
@UiThread
|
||||||
void onBluetoothDiscoverable(ContactItem contact) {
|
void onBluetoothDiscoverable(ContactItem contact) {
|
||||||
connect(contact.getContact().getId());
|
contactId = contact.getContact().getId();
|
||||||
|
connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void connect(ContactId contactId) {
|
@Override
|
||||||
// TODO
|
public void eventOccurred(@NonNull Event e) {
|
||||||
// * enable bluetooth connections setting, if not enabled
|
if (e instanceof ConnectionOpenedEvent) {
|
||||||
// * wait for plugin to become active
|
ConnectionOpenedEvent c = (ConnectionOpenedEvent) e;
|
||||||
ioExecutor.execute(() -> {
|
if (c.getContactId().equals(contactId) && c.isIncoming() &&
|
||||||
Random r = new Random();
|
c.getTransportId() == ID) {
|
||||||
try {
|
if (bluetoothPlugin != null) {
|
||||||
showToast(R.string.toast_connect_via_bluetooth_start);
|
bluetoothPlugin.stopDiscoverAndConnect();
|
||||||
// TODO do real work here
|
|
||||||
Thread.sleep(r.nextInt(3000) + 3000);
|
|
||||||
if (r.nextBoolean()) {
|
|
||||||
showToast(R.string.toast_connect_via_bluetooth_success);
|
|
||||||
} else {
|
|
||||||
showToast(R.string.toast_connect_via_bluetooth_error);
|
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
LOG.info("Contact connected to us");
|
||||||
logException(LOG, WARNING, e);
|
showToast(R.string.toast_connect_via_bluetooth_success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect() {
|
||||||
|
pluginManager.setPluginEnabled(ID, true);
|
||||||
|
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
if (!waitForBluetoothActive()) {
|
||||||
|
showToast(R.string.bt_plugin_status_inactive);
|
||||||
|
LOG.warning("Bluetooth plugin didn't become active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(R.string.toast_connect_via_bluetooth_start);
|
||||||
|
eventBus.addListener(this);
|
||||||
|
try {
|
||||||
|
String uuid = null;
|
||||||
|
try {
|
||||||
|
uuid = transportPropertyManager
|
||||||
|
.getRemoteProperties(contactId, ID).get(PROP_UUID);
|
||||||
|
} catch (DbException e) {
|
||||||
|
logException(LOG, WARNING, e);
|
||||||
|
}
|
||||||
|
if (isNullOrEmpty(uuid)) {
|
||||||
|
LOG.warning("PROP_UUID missing for contact");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DuplexTransportConnection conn = bluetoothPlugin
|
||||||
|
.discoverAndConnectForSetup(uuid);
|
||||||
|
if (conn == null) {
|
||||||
|
if (!isConnectedViaBluetooth(contactId)) {
|
||||||
|
LOG.warning("Failed to connect");
|
||||||
|
showToast(R.string.toast_connect_via_bluetooth_error);
|
||||||
|
} else {
|
||||||
|
LOG.info("Failed to connect, but contact connected");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectionManager.manageOutgoingConnection(contactId, ID, conn);
|
||||||
|
showToast(R.string.toast_connect_via_bluetooth_success);
|
||||||
|
} finally {
|
||||||
|
eventBus.removeListener(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean waitForBluetoothActive() {
|
||||||
|
long left = BT_ACTIVE_TIMEOUT;
|
||||||
|
final long sleep = 250;
|
||||||
|
try {
|
||||||
|
while (left > 0) {
|
||||||
|
if (bluetoothPlugin.getState() == ACTIVE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Thread.sleep(sleep);
|
||||||
|
left -= sleep;
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
return (bluetoothPlugin.getState() == ACTIVE);
|
||||||
|
}
|
||||||
|
|
||||||
private void showToast(@StringRes int res) {
|
private void showToast(@StringRes int res) {
|
||||||
androidExecutor.runOnUiThread(() ->
|
androidExecutor.runOnUiThread(() ->
|
||||||
Toast.makeText(app, res, Toast.LENGTH_LONG).show()
|
Toast.makeText(app, res, Toast.LENGTH_LONG).show()
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ public class BluetoothConnecterDialogFragment extends DialogFragment {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bluetoothConnecter.isDiscovering()) {
|
if (bluetoothConnecter.isDiscovering()) {
|
||||||
// TODO showToast(R.string.toast_connect_via_bluetooth_discovering);
|
showToast(R.string.toast_connect_via_bluetooth_already_discovering);
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import org.briarproject.bramble.api.event.EventBus;
|
|||||||
import org.briarproject.bramble.api.event.EventListener;
|
import org.briarproject.bramble.api.event.EventListener;
|
||||||
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
|
||||||
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
|
||||||
|
import org.briarproject.bramble.api.plugin.BluetoothConstants;
|
||||||
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
|
import org.briarproject.bramble.api.plugin.event.ContactConnectedEvent;
|
||||||
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
import org.briarproject.bramble.api.plugin.event.ContactDisconnectedEvent;
|
||||||
import org.briarproject.bramble.api.sync.ClientId;
|
import org.briarproject.bramble.api.sync.ClientId;
|
||||||
|
|||||||
@@ -175,6 +175,7 @@
|
|||||||
<string name="menu_item_connect_via_bluetooth">Connect via Bluetooth</string>
|
<string name="menu_item_connect_via_bluetooth">Connect via Bluetooth</string>
|
||||||
<string name="dialog_title_connect_via_bluetooth">Connect via Bluetooth</string>
|
<string name="dialog_title_connect_via_bluetooth">Connect via Bluetooth</string>
|
||||||
<string name="dialog_message_connect_via_bluetooth">Your contact needs to be nearby for this to work.\n\nYou and your contact should both press \"Start\" at the same time.</string>
|
<string name="dialog_message_connect_via_bluetooth">Your contact needs to be nearby for this to work.\n\nYou and your contact should both press \"Start\" at the same time.</string>
|
||||||
|
<string name="toast_connect_via_bluetooth_already_discovering">Already trying to connect via Bluetooth</string>
|
||||||
<string name="toast_connect_via_bluetooth_not_discoverable">Cannot continue without Bluetooth</string>
|
<string name="toast_connect_via_bluetooth_not_discoverable">Cannot continue without Bluetooth</string>
|
||||||
<string name="toast_connect_via_bluetooth_no_location_permission">Cannot continue without location permission</string>
|
<string name="toast_connect_via_bluetooth_no_location_permission">Cannot continue without location permission</string>
|
||||||
<string name="toast_connect_via_bluetooth_start">Connecting via Bluetooth…</string>
|
<string name="toast_connect_via_bluetooth_start">Connecting via Bluetooth…</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user