From e2a11d42f8d2a5482962c0f2c7f52bc25c77e1e0 Mon Sep 17 00:00:00 2001 From: Daniel Lublin Date: Sat, 17 Apr 2021 10:29:12 +0200 Subject: [PATCH] Implement backend for connect via bluetooth --- .../bluetooth/AndroidBluetoothPlugin.java | 41 +- .../bluetooth/AbstractBluetoothPlugin.java | 623 ++++++++++++++++++ .../plugin/bluetooth/BluetoothPlugin.java | 598 +---------------- .../plugin/bluetooth/JavaBluetoothPlugin.java | 9 +- .../nearby/AddNearbyContactIntroFragment.java | 1 + .../add/nearby/AddNearbyContactViewModel.java | 18 +- .../conversation/BluetoothConnecter.java | 133 +++- .../BluetoothConnecterDialogFragment.java | 2 +- .../conversation/ConversationActivity.java | 1 + briar-android/src/main/res/values/strings.xml | 1 + 10 files changed, 789 insertions(+), 638 deletions(-) create mode 100644 bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java index 9df713343..ca66eeb03 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPlugin.java @@ -59,8 +59,8 @@ import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; @MethodsNotNullByDefault @ParametersNotNullByDefault -class AndroidBluetoothPlugin - extends BluetoothPlugin { +class AndroidBluetoothPlugin extends + AbstractBluetoothPlugin { private static final Logger LOG = getLogger(AndroidBluetoothPlugin.class.getName()); @@ -75,6 +75,7 @@ class AndroidBluetoothPlugin // Non-null if the plugin started successfully private volatile BluetoothAdapter adapter = null; + private volatile boolean stopDiscoverAndConnect; AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter, BluetoothConnectionFactory connectionFactory, @@ -187,22 +188,40 @@ class AndroidBluetoothPlugin @Nullable DuplexTransportConnection discoverAndConnect(String uuid) { if (adapter == null) return null; - for (String address : discoverDevices()) { - 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)); + if (!discoverSemaphore.tryAcquire()) { + LOG.info("Discover already running"); + return null; + } + try { + stopDiscoverAndConnect = false; + for (String address : discoverDevices()) { + if (stopDiscoverAndConnect) { + 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"); return null; } + @Override + public void stopDiscoverAndConnect() { + stopDiscoverAndConnect = true; + adapter.cancelDiscovery(); + } + private Collection discoverDevices() { List addresses = new ArrayList<>(); BlockingQueue intents = new LinkedBlockingQueue<>(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java new file mode 100644 index 000000000..91f0860a3 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java @@ -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 implements BluetoothPlugin, + EventListener { + + private static final Logger LOG = + getLogger(AbstractBluetoothPlugin.class.getName()); + + private final BluetoothConnectionLimiter connectionLimiter; + final BluetoothConnectionFactory 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 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 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> + properties) { + if (getState() != ACTIVE) return; + backoff.increment(); + for (Pair 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; + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java index f90469d56..1350ee128 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/BluetoothPlugin.java @@ -1,606 +1,18 @@ 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.DuplexPlugin; 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.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; +@NotNullByDefault +public interface BluetoothPlugin extends DuplexPlugin { -@MethodsNotNullByDefault -@ParametersNotNullByDefault -abstract class BluetoothPlugin implements DuplexPlugin, EventListener { - - private static final Logger LOG = - getLogger(BluetoothPlugin.class.getName()); - - final BluetoothConnectionLimiter connectionLimiter; - final BluetoothConnectionFactory 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; + boolean isDiscovering(); @Nullable - abstract DuplexTransportConnection discoverAndConnect(String uuid); + DuplexTransportConnection discoverAndConnectForSetup(String uuid); - BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter, - BluetoothConnectionFactory 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 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> - properties) { - if (getState() != ACTIVE) return; - backoff.increment(); - for (Pair 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; - } - } + void stopDiscoverAndConnect(); } diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java index d76e4ee0a..cc80027fe 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java @@ -25,8 +25,8 @@ import static org.briarproject.bramble.util.StringUtils.isValidMac; @MethodsNotNullByDefault @ParametersNotNullByDefault -class JavaBluetoothPlugin - extends BluetoothPlugin { +class JavaBluetoothPlugin extends + AbstractBluetoothPlugin { private static final Logger LOG = getLogger(JavaBluetoothPlugin.class.getName()); @@ -108,6 +108,11 @@ class JavaBluetoothPlugin return null; // TODO } + @Override + public void stopDiscoverAndConnect() { + // TODO + } + private String makeUrl(String address, String uuid) { return "btspp://" + address + ":" + uuid + ";name=RFCOMM"; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java index 5eebfcada..e89d691ec 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactIntroFragment.java @@ -73,6 +73,7 @@ public class AddNearbyContactIntroFragment extends BaseFragment { scrollView = v.findViewById(R.id.scrollView); View button = v.findViewById(R.id.continueButton); button.setOnClickListener(view -> { + viewModel.stopDiscovery(); viewModel.onContinueClicked(); if (permissionManager.checkPermissions()) { viewModel.showQrCodeFragmentIfAllowed(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java index c1a3f76af..d6d225100 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java @@ -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.event.TransportStateEvent; import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.bramble.plugin.bluetooth.BluetoothPlugin; import org.briarproject.briar.R; import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeFinished; import org.briarproject.briar.android.contact.add.nearby.AddContactState.ContactExchangeResult.Error; @@ -149,7 +150,9 @@ class AddNearbyContactViewModel extends AndroidViewModel @Nullable private final BluetoothAdapter bt; @Nullable // UiThread - private Plugin wifiPlugin, bluetoothPlugin; + private Plugin wifiPlugin; + @Nullable // UiThread + private BluetoothPlugin bluetoothPlugin; // UiThread private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN; @@ -195,7 +198,8 @@ class AddNearbyContactViewModel extends AndroidViewModel this.connectionManager = connectionManager; bt = BluetoothAdapter.getDefaultAdapter(); wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID); - bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + bluetoothPlugin = (BluetoothPlugin) pluginManager + .getPlugin(BluetoothConstants.ID); qrCodeDecoder = new QrCodeDecoder(androidExecutor, ioExecutor, this); eventBus.addListener(this); IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); @@ -218,7 +222,8 @@ class AddNearbyContactViewModel extends AndroidViewModel @UiThread void resetPlugins() { wifiPlugin = pluginManager.getPlugin(LanTcpConstants.ID); - bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + bluetoothPlugin = (BluetoothPlugin) pluginManager + .getPlugin(BluetoothConstants.ID); } @UiThread @@ -375,6 +380,13 @@ class AddNearbyContactViewModel extends AndroidViewModel } } + void stopDiscovery() { + if (!isBluetoothSupported() || !bluetoothPlugin.isDiscovering()) { + return; + } + bluetoothPlugin.stopDiscoverAndConnect(); + } + @SuppressWarnings("StatementWithEmptyBody") @UiThread void showQrCodeFragmentIfAllowed() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java index 050a91e50..f53fe0edb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecter.java @@ -6,23 +6,30 @@ import android.bluetooth.BluetoothAdapter; import android.content.Context; import android.widget.Toast; +import org.briarproject.bramble.api.connection.ConnectionManager; import org.briarproject.bramble.api.connection.ConnectionRegistry; 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.plugin.BluetoothConstants; -import org.briarproject.bramble.api.plugin.Plugin; 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.plugin.bluetooth.BluetoothPlugin; import org.briarproject.briar.R; import org.briarproject.briar.android.contact.ContactItem; -import java.util.Random; import java.util.concurrent.Executor; import java.util.logging.Logger; import javax.inject.Inject; import androidx.activity.result.ActivityResultLauncher; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.UiThread; @@ -31,17 +38,24 @@ import androidx.appcompat.app.AlertDialog; import static android.Manifest.permission.ACCESS_FINE_LOCATION; import static android.os.Build.VERSION.SDK_INT; 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.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.StringUtils.isNullOrEmpty; 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.showLocationDialog; -class BluetoothConnecter { +class BluetoothConnecter implements EventListener { private final Logger LOG = getLogger(BluetoothConnecter.class.getName()); + private final long BT_ACTIVE_TIMEOUT = SECONDS.toMillis(5); + private enum Permission { UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED } @@ -52,32 +66,41 @@ class BluetoothConnecter { private final AndroidExecutor androidExecutor; private final ConnectionRegistry connectionRegistry; 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 ContactId contactId = null; @Inject BluetoothConnecter(Application app, PluginManager pluginManager, @IoExecutor Executor ioExecutor, AndroidExecutor androidExecutor, - ConnectionRegistry connectionRegistry) { + ConnectionRegistry connectionRegistry, + EventBus eventBus, + TransportPropertyManager transportPropertyManager, + ConnectionManager connectionManager) { this.app = app; this.pluginManager = pluginManager; this.ioExecutor = ioExecutor; this.androidExecutor = androidExecutor; - this.bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + this.bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID); this.connectionRegistry = connectionRegistry; + this.eventBus = eventBus; + this.transportPropertyManager = transportPropertyManager; + this.connectionManager = connectionManager; } boolean isConnectedViaBluetooth(ContactId contactId) { - return connectionRegistry.isConnected(contactId, BluetoothConstants.ID); + return connectionRegistry.isConnected(contactId, ID); } boolean isDiscovering() { - // TODO bluetoothPlugin.isDiscovering() - return false; + return bluetoothPlugin.isDiscovering(); } /** @@ -89,7 +112,7 @@ class BluetoothConnecter { // When this class is instantiated before we are logged in // (like when returning to a killed activity), bluetoothPlugin would be // null and we consider bluetooth not supported. So reset here. - bluetoothPlugin = pluginManager.getPlugin(BluetoothConstants.ID); + bluetoothPlugin = (BluetoothPlugin) pluginManager.getPlugin(ID); } @UiThread @@ -149,30 +172,84 @@ class BluetoothConnecter { @UiThread void onBluetoothDiscoverable(ContactItem contact) { - connect(contact.getContact().getId()); + contactId = contact.getContact().getId(); + connect(); } - private void connect(ContactId contactId) { - // TODO - // * enable bluetooth connections setting, if not enabled - // * wait for plugin to become active - ioExecutor.execute(() -> { - Random r = new Random(); - try { - showToast(R.string.toast_connect_via_bluetooth_start); - // 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); + @Override + public void eventOccurred(@NonNull Event e) { + if (e instanceof ConnectionOpenedEvent) { + ConnectionOpenedEvent c = (ConnectionOpenedEvent) e; + if (c.getContactId().equals(contactId) && c.isIncoming() && + c.getTransportId() == ID) { + if (bluetoothPlugin != null) { + bluetoothPlugin.stopDiscoverAndConnect(); } - } catch (InterruptedException e) { - logException(LOG, WARNING, e); + LOG.info("Contact connected to us"); + 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) { androidExecutor.runOnUiThread(() -> Toast.makeText(app, res, Toast.LENGTH_LONG).show() diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java index 30d3b781e..955dc419f 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/BluetoothConnecterDialogFragment.java @@ -91,7 +91,7 @@ public class BluetoothConnecterDialogFragment extends DialogFragment { return; } if (bluetoothConnecter.isDiscovering()) { - // TODO showToast(R.string.toast_connect_via_bluetooth_discovering); + showToast(R.string.toast_connect_via_bluetooth_already_discovering); dismiss(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 83fed88c9..448d44b57 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -34,6 +34,7 @@ import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; 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.ContactDisconnectedEvent; import org.briarproject.bramble.api.sync.ClientId; diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index f7c84824f..5d0e5c0ec 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -175,6 +175,7 @@ Connect via Bluetooth 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. + Already trying to connect via Bluetooth Cannot continue without Bluetooth Cannot continue without location permission Connecting via Bluetooth…