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 71992283e..560aafd3a 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 @@ -154,6 +154,12 @@ class AndroidBluetoothPlugin extends BluetoothPlugin { wasEnabledByUs = true; } + @Override + void onAdapterDisabled() { + super.onAdapterDisabled(); + wasEnabledByUs = false; + } + @Override @Nullable String getBluetoothAddress() { diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java index d243668ee..cabb2f653 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPlugin.java @@ -1,23 +1,30 @@ package org.briarproject.bramble.plugin.tcp; +import android.annotation.TargetApi; import android.content.Context; import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.LinkProperties; import android.net.Network; -import android.net.NetworkInfo; +import android.net.NetworkCapabilities; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import org.briarproject.bramble.PoliteExecutor; +import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.network.event.NetworkStatusEvent; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.PluginCallback; +import org.briarproject.bramble.api.settings.Settings; import java.io.IOException; import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; import java.net.Socket; +import java.net.SocketException; import java.net.UnknownHostException; import java.util.List; import java.util.concurrent.Executor; @@ -28,14 +35,20 @@ import javax.net.SocketFactory; import static android.content.Context.CONNECTIVITY_SERVICE; import static android.content.Context.WIFI_SERVICE; -import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.os.Build.VERSION.SDK_INT; import static java.util.Collections.emptyList; +import static java.util.Collections.list; import static java.util.Collections.singletonList; +import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE; +import static org.briarproject.bramble.util.IoUtils.tryToClose; +import static org.briarproject.bramble.util.LogUtils.logException; @NotNullByDefault -class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener { +class AndroidLanTcpPlugin extends LanTcpPlugin { private static final Logger LOG = getLogger(AndroidLanTcpPlugin.class.getName()); @@ -68,37 +81,136 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener { public void start() { if (used.getAndSet(true)) throw new IllegalStateException(); initialisePortProperty(); - running = true; + Settings settings = callback.getSettings(); + state.setStarted(settings.getBoolean(PREF_PLUGIN_ENABLE, false)); updateConnectionStatus(); } - @Override - public void stop() { - running = false; - tryToClose(socket); - } - @Override protected Socket createSocket() throws IOException { return socketFactory.createSocket(); } @Override - protected List getUsableLocalInetAddresses() { - // If the device doesn't have wifi, don't open any sockets - if (wifiManager == null) return emptyList(); + protected List getUsableLocalInetAddresses(boolean ipv4) { + InetAddress addr = getWifiAddress(ipv4); + return addr == null ? emptyList() : singletonList(addr); + } + + @Nullable + private InetAddress getWifiAddress(boolean ipv4) { + Pair wifi = getWifiIpv4Address(); + if (ipv4) return wifi == null ? null : wifi.getFirst(); + // If there's no wifi IPv4 address, we might be a client on an + // IPv6-only wifi network. We can only detect this on API 21+ + if (wifi == null) { + return SDK_INT >= 21 ? getWifiClientIpv6Address() : null; + } + // Use the wifi IPv4 address to determine which interface's IPv6 + // address we should return (if the interface has a suitable address) + return getIpv6AddressForInterface(wifi.getFirst()); + } + + /** + * Returns a {@link Pair} where the first element is the IPv4 address of + * the wifi interface and the second element is true if this device is + * providing an access point, or false if this device is a client. Returns + * null if this device isn't connected to wifi as an access point or client. + */ + @Nullable + private Pair getWifiIpv4Address() { + if (wifiManager == null) return null; // If we're connected to a wifi network, return its address WifiInfo info = wifiManager.getConnectionInfo(); if (info != null && info.getIpAddress() != 0) { - return singletonList(intToInetAddress(info.getIpAddress())); + return new Pair<>(intToInetAddress(info.getIpAddress()), false); } - // If we're running an access point, return its address - for (InetAddress addr : getLocalInetAddresses()) { - if (addr.equals(WIFI_AP_ADDRESS)) return singletonList(addr); - if (addr.equals(WIFI_DIRECT_AP_ADDRESS)) return singletonList(addr); + List ifAddrs = getLocalInterfaceAddresses(); + // If we're providing a normal access point, return its address + for (InterfaceAddress ifAddr : ifAddrs) { + if (isAndroidWifiApAddress(ifAddr)) { + return new Pair<>(ifAddr.getAddress(), true); + } + } + // If we're providing a wifi direct access point, return its address + for (InterfaceAddress ifAddr : ifAddrs) { + if (isAndroidWifiDirectApAddress(ifAddr)) { + return new Pair<>(ifAddr.getAddress(), true); + } + } + // Not connected to wifi + return null; + } + + /** + * Returns true if the given address belongs to a network provided by an + * Android access point (including the access point's own address). + *

+ * The access point's address is usually 192.168.43.1, but at least one + * device (Honor 8A) may use other addresses in the range 192.168.43.0/24. + */ + private boolean isAndroidWifiApAddress(InterfaceAddress ifAddr) { + if (ifAddr.getNetworkPrefixLength() != 24) return false; + byte[] ip = ifAddr.getAddress().getAddress(); + return ip.length == 4 + && ip[0] == (byte) 192 + && ip[1] == (byte) 168 + && ip[2] == (byte) 43; + } + + /** + * Returns true if the given address belongs to a network provided by an + * Android wifi direct legacy mode access point (including the access + * point's own address). + */ + private boolean isAndroidWifiDirectApAddress(InterfaceAddress ifAddr) { + if (ifAddr.getNetworkPrefixLength() != 24) return false; + byte[] ip = ifAddr.getAddress().getAddress(); + return ip.length == 4 + && ip[0] == (byte) 192 + && ip[1] == (byte) 168 + && ip[2] == (byte) 49; + } + + /** + * Returns a link-local IPv6 address for the wifi client interface, or null + * if there's no such interface or it doesn't have a suitable address. + */ + @TargetApi(21) + @Nullable + private InetAddress getWifiClientIpv6Address() { + for (Network net : connectivityManager.getAllNetworks()) { + NetworkCapabilities caps = + connectivityManager.getNetworkCapabilities(net); + if (caps == null || !caps.hasTransport(TRANSPORT_WIFI)) continue; + LinkProperties props = connectivityManager.getLinkProperties(net); + if (props == null) continue; + for (LinkAddress linkAddress : props.getLinkAddresses()) { + InetAddress addr = linkAddress.getAddress(); + if (isIpv6LinkLocalAddress(addr)) return addr; + } + } + return null; + } + + /** + * Returns a link-local IPv6 address for the interface with the given IPv4 + * address, or null if the interface doesn't have a suitable address. + */ + @Nullable + private InetAddress getIpv6AddressForInterface(InetAddress ipv4) { + try { + NetworkInterface iface = NetworkInterface.getByInetAddress(ipv4); + if (iface == null) return null; + for (InetAddress addr : list(iface.getInetAddresses())) { + if (isIpv6LinkLocalAddress(addr)) return addr; + } + // No suitable address + return null; + } catch (SocketException e) { + logException(LOG, WARNING, e); + return null; } - // No suitable addresses - return emptyList(); } private InetAddress intToInetAddress(int ip) { @@ -120,9 +232,11 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener { private SocketFactory getSocketFactory() { if (SDK_INT < 21) return SocketFactory.getDefault(); for (Network net : connectivityManager.getAllNetworks()) { - NetworkInfo info = connectivityManager.getNetworkInfo(net); - if (info != null && info.getType() == TYPE_WIFI) + NetworkCapabilities caps = + connectivityManager.getNetworkCapabilities(net); + if (caps != null && caps.hasTransport(TRANSPORT_WIFI)) { return net.getSocketFactory(); + } } LOG.warning("Could not find suitable socket factory"); return SocketFactory.getDefault(); @@ -130,31 +244,59 @@ class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener { @Override public void eventOccurred(Event e) { + super.eventOccurred(e); if (e instanceof NetworkStatusEvent) updateConnectionStatus(); } private void updateConnectionStatus() { connectionStatusExecutor.execute(() -> { - if (!running) return; - List addrs = getUsableLocalInetAddresses(); - if (addrs.contains(WIFI_AP_ADDRESS) - || addrs.contains(WIFI_DIRECT_AP_ADDRESS)) { + State s = getState(); + if (s != ACTIVE && s != INACTIVE) return; + Pair wifi = getPreferredWifiAddress(); + if (wifi == null) { + LOG.info("Not connected to wifi"); + socketFactory = SocketFactory.getDefault(); + // Server sockets may not have been closed automatically when + // interface was taken down. If any sockets are open, closing + // them here will cause the sockets to be cleared and the state + // to be updated in acceptContactConnections() + if (s == ACTIVE) { + LOG.info("Closing server sockets"); + tryToClose(state.getServerSocket(true), LOG, WARNING); + tryToClose(state.getServerSocket(false), LOG, WARNING); + } + } else if (wifi.getSecond()) { LOG.info("Providing wifi hotspot"); // There's no corresponding Network object and thus no way // to get a suitable socket factory, so we won't be able to // make outgoing connections on API 21+ if another network // has internet access socketFactory = SocketFactory.getDefault(); - if (socket == null || socket.isClosed()) bind(); - } else if (addrs.isEmpty()) { - LOG.info("Not connected to wifi"); - socketFactory = SocketFactory.getDefault(); - tryToClose(socket); + if (s == INACTIVE) bind(); } else { LOG.info("Connected to wifi"); socketFactory = getSocketFactory(); - if (socket == null || socket.isClosed()) bind(); + if (s == INACTIVE) bind(); } }); } + + /** + * Returns a {@link Pair} where the first element is an IP address (IPv4 if + * available, otherwise IPv6) of the wifi interface and the second element + * is true if this device is providing an access point, or false if this + * device is a client. Returns null if this device isn't connected to wifi + * as an access point or client. + */ + @Nullable + private Pair getPreferredWifiAddress() { + Pair wifi = getWifiIpv4Address(); + // If there's no wifi IPv4 address, we might be a client on an + // IPv6-only wifi network. We can only detect this on API 21+ + if (wifi == null && SDK_INT >= 21) { + InetAddress ipv6 = getWifiClientIpv6Address(); + if (ipv6 != null) return new Pair<>(ipv6, false); + } + return wifi; + } } \ No newline at end of file diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java index bf1352479..68dc17855 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/AndroidTorPlugin.java @@ -75,7 +75,6 @@ class AndroidTorPlugin extends TorPlugin { @Override protected void enableNetwork(boolean enable) throws IOException { - if (!running) return; if (enable) wakeLock.acquire(); super.enableNetwork(enable); if (!enable) wakeLock.release(); diff --git a/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidLocationUtils.java b/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidLocationUtils.java index 226064d6f..21d2075c1 100644 --- a/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidLocationUtils.java +++ b/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidLocationUtils.java @@ -61,12 +61,12 @@ class AndroidLocationUtils implements LocationUtils { private String getCountryFromPhoneNetwork() { Object o = appContext.getSystemService(TELEPHONY_SERVICE); TelephonyManager tm = (TelephonyManager) o; - return tm.getNetworkCountryIso(); + return tm == null ? "" : tm.getNetworkCountryIso(); } private String getCountryFromSimCard() { Object o = appContext.getSystemService(TELEPHONY_SERVICE); TelephonyManager tm = (TelephonyManager) o; - return tm.getSimCountryIso(); + return tm == null ? "" : tm.getSimCountryIso(); } } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/event/EventBus.java b/bramble-api/src/main/java/org/briarproject/bramble/api/event/EventBus.java index d27449833..cf57b67ed 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/event/EventBus.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/event/EventBus.java @@ -18,6 +18,8 @@ public interface EventBus { /** * Asynchronously notifies all listeners of an event. Listeners are * notified on the {@link EventExecutor}. + *

+ * This method can safely be called while holding a lock. */ void broadcast(Event e); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/BluetoothConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/BluetoothConstants.java index 785aee2b3..c2e506851 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/BluetoothConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/BluetoothConstants.java @@ -8,6 +8,4 @@ public interface BluetoothConstants { String PROP_ADDRESS = "address"; String PROP_UUID = "uuid"; - - String PREF_BT_ENABLE = "enable"; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/LanTcpConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/LanTcpConstants.java index d546a60cd..69880b4ab 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/LanTcpConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/LanTcpConstants.java @@ -7,7 +7,9 @@ public interface LanTcpConstants { // Transport properties (shared with contacts) String PROP_IP_PORTS = "ipPorts"; String PROP_PORT = "port"; + String PROP_IPV6 = "ipv6"; - // A local setting + // Local settings (not shared with contacts) String PREF_LAN_IP_PORTS = "ipPorts"; + String PREF_IPV6 = "ipv6"; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java index 84872dfed..9d2d50600 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/Plugin.java @@ -3,12 +3,55 @@ package org.briarproject.bramble.api.plugin; import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.settings.SettingsManager; import java.util.Collection; @NotNullByDefault public interface Plugin { + enum State { + + /** + * The plugin has not finished starting or has been stopped. + */ + STARTING_STOPPING, + + /** + * The plugin is disabled by settings. Use {@link #getReasonsDisabled()} + * to find out which settings are responsible. + */ + DISABLED, + + /** + * The plugin is being enabled and can't yet make or receive + * connections. + */ + ENABLING, + + /** + * The plugin is enabled and can make or receive connections. + */ + ACTIVE, + + /** + * The plugin is enabled but can't make or receive connections + */ + INACTIVE + } + + /** + * The string for the boolean preference + * to use with the {@link SettingsManager} to enable or disable the plugin. + */ + String PREF_PLUGIN_ENABLE = "enable"; + + /** + * Reason flag returned by {@link #getReasonsDisabled()} to indicate that + * the plugin has been disabled by the user. + */ + int REASON_USER = 1; + /** * Returns the plugin's transport identifier. */ @@ -35,9 +78,18 @@ public interface Plugin { void stop() throws PluginException; /** - * Returns true if the plugin is running. + * Returns the current state of the plugin. */ - boolean isRunning(); + State getState(); + + /** + * Returns a set of flags indicating why the plugin is + * {@link State#DISABLED disabled}, or 0 if the plugin is not disabled. + *

+ * The flags used are plugin-specific, except the generic flag + * {@link #REASON_USER}, which may be used by any plugin. + */ + int getReasonsDisabled(); /** * Returns true if the plugin should be polled periodically to attempt to diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginCallback.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginCallback.java index b9cbd450b..1b149cde1 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginCallback.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginCallback.java @@ -1,6 +1,10 @@ package org.briarproject.bramble.api.plugin; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.Plugin.State; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportStateEvent; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.settings.Settings; @@ -32,12 +36,17 @@ public interface PluginCallback extends ConnectionHandler { void mergeLocalProperties(TransportProperties p); /** - * Signals that the transport is enabled. + * Informs the callback of the plugin's current state. + *

+ * If the current state is different from the previous state, the callback + * will broadcast a {@link TransportStateEvent}. If the current state is + * {@link State#ACTIVE} and the previous state was not + * {@link State#ACTIVE}, the callback will broadcast a + * {@link TransportActiveEvent}. If the current state is not + * {@link State#ACTIVE} and the previous state was {@link State#ACTIVE}, + * the callback will broadcast a {@link TransportInactiveEvent}. + *

+ * This method can safely be called while holding a lock. */ - void transportEnabled(); - - /** - * Signals that the transport is disabled. - */ - void transportDisabled(); + void pluginStateChanged(State state); } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginManager.java index 9c34b434d..5132ba3c6 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/PluginManager.java @@ -41,4 +41,17 @@ public interface PluginManager { * Returns any duplex plugins that support rendezvous. */ Collection getRendezvousPlugins(); + + /** + * Enables or disables the plugin + * identified by the given {@link TransportId}. + *

+ * Note that this applies the change asynchronously + * and there are no order guarantees. + *

+ * If no plugin with the given {@link TransportId} is registered, + * this is a no-op. + */ + void setPluginEnabled(TransportId t, boolean enabled); + } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TorConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TorConstants.java index 6be7582a1..e22d6da4a 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TorConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/TorConstants.java @@ -21,6 +21,21 @@ public interface TorConstants { int PREF_TOR_NETWORK_AUTOMATIC = 0; int PREF_TOR_NETWORK_WITHOUT_BRIDGES = 1; int PREF_TOR_NETWORK_WITH_BRIDGES = 2; + // TODO: Remove when settings migration code is removed int PREF_TOR_NETWORK_NEVER = 3; + /** + * Reason flag returned by {@link Plugin#getReasonsDisabled()}. + */ + int REASON_BATTERY = 2; + + /** + * Reason flag returned by {@link Plugin#getReasonsDisabled()}. + */ + int REASON_MOBILE_DATA = 4; + + /** + * Reason flag returned by {@link Plugin#getReasonsDisabled()}. + */ + int REASON_COUNTRY_BLOCKED = 8; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportDisabledEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportActiveEvent.java similarity index 64% rename from bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportDisabledEvent.java rename to bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportActiveEvent.java index 1d415ca9f..de9c49c90 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportDisabledEvent.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportActiveEvent.java @@ -2,20 +2,22 @@ package org.briarproject.bramble.api.plugin.event; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.Plugin.State; import org.briarproject.bramble.api.plugin.TransportId; import javax.annotation.concurrent.Immutable; /** - * An event that is broadcast when a transport is disabled. + * An event that is broadcast when a plugin enters the {@link State#ACTIVE} + * state. */ @Immutable @NotNullByDefault -public class TransportDisabledEvent extends Event { +public class TransportActiveEvent extends Event { private final TransportId transportId; - public TransportDisabledEvent(TransportId transportId) { + public TransportActiveEvent(TransportId transportId) { this.transportId = transportId; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportEnabledEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportInactiveEvent.java similarity index 64% rename from bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportEnabledEvent.java rename to bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportInactiveEvent.java index 7064c9b9a..e2167c0f5 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportEnabledEvent.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportInactiveEvent.java @@ -2,20 +2,22 @@ package org.briarproject.bramble.api.plugin.event; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.Plugin.State; import org.briarproject.bramble.api.plugin.TransportId; import javax.annotation.concurrent.Immutable; /** - * An event that is broadcast when a transport is enabled. + * An event that is broadcast when a plugin leaves the {@link State#ACTIVE} + * state. */ @Immutable @NotNullByDefault -public class TransportEnabledEvent extends Event { +public class TransportInactiveEvent extends Event { private final TransportId transportId; - public TransportEnabledEvent(TransportId transportId) { + public TransportInactiveEvent(TransportId transportId) { this.transportId = transportId; } diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportStateEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportStateEvent.java new file mode 100644 index 000000000..376b5303f --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/plugin/event/TransportStateEvent.java @@ -0,0 +1,32 @@ +package org.briarproject.bramble.api.plugin.event; + +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.Plugin.State; +import org.briarproject.bramble.api.plugin.TransportId; + +import javax.annotation.concurrent.Immutable; + +/** + * An event that is broadcast when the {@link State state} of a plugin changes. + */ +@Immutable +@NotNullByDefault +public class TransportStateEvent extends Event { + + private final TransportId transportId; + private final State state; + + public TransportStateEvent(TransportId transportId, State state) { + this.transportId = transportId; + this.state = state; + } + + public TransportId getTransportId() { + return transportId; + } + + public State getState() { + return state; + } +} diff --git a/bramble-api/src/main/java/org/briarproject/bramble/util/PrivacyUtils.java b/bramble-api/src/main/java/org/briarproject/bramble/util/PrivacyUtils.java index 9156b9caa..10df6686c 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/util/PrivacyUtils.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/util/PrivacyUtils.java @@ -2,13 +2,17 @@ package org.briarproject.bramble.util; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import java.net.Inet6Address; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import javax.annotation.Nullable; +import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; +import static org.briarproject.bramble.util.StringUtils.isValidMac; +import static org.briarproject.bramble.util.StringUtils.toHexString; + @NotNullByDefault public class PrivacyUtils { @@ -19,7 +23,7 @@ public class PrivacyUtils { @Nullable public static String scrubMacAddress(@Nullable String address) { - if (address == null || address.length() == 0) return null; + if (isNullOrEmpty(address) || !isValidMac(address)) return address; // this is a fake address we need to know about if (address.equals("02:00:00:00:00:00")) return address; // keep first and last octet of MAC address @@ -27,39 +31,37 @@ public class PrivacyUtils { + address.substring(14, 17); } - @Nullable public static String scrubInetAddress(InetAddress address) { - // don't scrub link and site local addresses - if (address.isLinkLocalAddress() || address.isSiteLocalAddress()) - return address.toString(); - // completely scrub IPv6 addresses - if (address instanceof Inet6Address) return "[scrubbed]"; - // keep first and last octet of IPv4 addresses - return scrubInetAddress(address.toString()); + if (address instanceof Inet4Address) { + // Don't scrub local IPv4 addresses + if (address.isLoopbackAddress() || address.isLinkLocalAddress() || + address.isSiteLocalAddress()) { + return address.getHostAddress(); + } + // Keep first and last octet of non-local IPv4 addresses + return scrubIpv4Address(address.getAddress()); + } else { + // Keep first and last octet of IPv6 addresses + return scrubIpv6Address(address.getAddress()); + } } - @Nullable - public static String scrubInetAddress(@Nullable String address) { - if (address == null) return null; - - int firstDot = address.indexOf("."); - if (firstDot == -1) return "[scrubbed]"; - String prefix = address.substring(0, firstDot + 1); - int lastDot = address.lastIndexOf("."); - String suffix = address.substring(lastDot, address.length()); - return prefix + "[scrubbed]" + suffix; + private static String scrubIpv4Address(byte[] ipv4) { + return (ipv4[0] & 0xFF) + ".[scrubbed]." + (ipv4[3] & 0xFF); + } + + private static String scrubIpv6Address(byte[] ipv6) { + String hex = toHexString(ipv6).toLowerCase(); + return hex.substring(0, 2) + "[scrubbed]" + hex.substring(30); } - @Nullable public static String scrubSocketAddress(InetSocketAddress address) { - InetAddress inetAddress = address.getAddress(); - return scrubInetAddress(inetAddress); + return scrubInetAddress(address.getAddress()); } - @Nullable public static String scrubSocketAddress(SocketAddress address) { if (address instanceof InetSocketAddress) return scrubSocketAddress((InetSocketAddress) address); - return scrubInetAddress(address.toString()); + return "[scrubbed]"; } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java index c7b33692a..f2d0d6a72 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PluginManagerImpl.java @@ -8,6 +8,7 @@ import org.briarproject.bramble.api.lifecycle.Service; import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.Plugin; +import org.briarproject.bramble.api.plugin.Plugin.State; import org.briarproject.bramble.api.plugin.PluginCallback; import org.briarproject.bramble.api.plugin.PluginConfig; import org.briarproject.bramble.api.plugin.PluginException; @@ -18,8 +19,9 @@ import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; -import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportStateEvent; import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; import org.briarproject.bramble.api.properties.TransportProperties; @@ -36,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Logger; import javax.annotation.concurrent.ThreadSafe; @@ -45,6 +48,9 @@ import static java.util.logging.Level.FINE; 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.plugin.Plugin.PREF_PLUGIN_ENABLE; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.LogUtils.now; @@ -177,6 +183,26 @@ class PluginManagerImpl implements PluginManager, Service { return supported; } + @Override + public void setPluginEnabled(TransportId t, boolean enabled) { + Plugin plugin = plugins.get(t); + if (plugin == null) return; + + Settings s = new Settings(); + s.putBoolean(PREF_PLUGIN_ENABLE, enabled); + ioExecutor.execute(() -> mergeSettings(s, t.getString())); + } + + private void mergeSettings(Settings s, String namespace) { + try { + long start = now(); + settingsManager.mergeSettings(s, namespace); + logDuration(LOG, "Merging settings", start); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + } + private class PluginStarter implements Runnable { private final Plugin plugin; @@ -250,7 +276,8 @@ class PluginManagerImpl implements PluginManager, Service { private class Callback implements PluginCallback { private final TransportId id; - private final AtomicBoolean enabled = new AtomicBoolean(false); + private final AtomicReference state = + new AtomicReference<>(STARTING_STOPPING); private Callback(TransportId id) { this.id = id; @@ -278,11 +305,7 @@ class PluginManagerImpl implements PluginManager, Service { @Override public void mergeSettings(Settings s) { - try { - settingsManager.mergeSettings(s, id.getString()); - } catch (DbException e) { - logException(LOG, WARNING, e); - } + PluginManagerImpl.this.mergeSettings(s, id.getString()); } @Override @@ -295,15 +318,20 @@ class PluginManagerImpl implements PluginManager, Service { } @Override - public void transportEnabled() { - if (!enabled.getAndSet(true)) - eventBus.broadcast(new TransportEnabledEvent(id)); - } - - @Override - public void transportDisabled() { - if (enabled.getAndSet(false)) - eventBus.broadcast(new TransportDisabledEvent(id)); + public void pluginStateChanged(State newState) { + State oldState = state.getAndSet(newState); + if (newState != oldState) { + if (LOG.isLoggable(INFO)) { + LOG.info(id + " changed from state " + oldState + + " to " + newState); + } + eventBus.broadcast(new TransportStateEvent(id, newState)); + if (newState == ACTIVE) { + eventBus.broadcast(new TransportActiveEvent(id)); + } else if (oldState == ACTIVE) { + eventBus.broadcast(new TransportInactiveEvent(id)); + } + } } @Override diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PollerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PollerImpl.java index 1aeb66172..58637ddca 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/PollerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/PollerImpl.java @@ -20,8 +20,8 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent; import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent; -import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportPropertyManager; @@ -106,13 +106,13 @@ class PollerImpl implements Poller, EventListener { ConnectionOpenedEvent c = (ConnectionOpenedEvent) e; // Reschedule polling, the polling interval may have decreased reschedule(c.getTransportId()); - } else if (e instanceof TransportEnabledEvent) { - TransportEnabledEvent t = (TransportEnabledEvent) e; - // Poll the newly enabled transport + } else if (e instanceof TransportActiveEvent) { + TransportActiveEvent t = (TransportActiveEvent) e; + // Poll the newly activated transport pollNow(t.getTransportId()); - } else if (e instanceof TransportDisabledEvent) { - TransportDisabledEvent t = (TransportDisabledEvent) e; - // Cancel polling for the disabled transport + } else if (e instanceof TransportInactiveEvent) { + TransportInactiveEvent t = (TransportInactiveEvent) e; + // Cancel polling for the deactivated transport cancel(t.getTransportId()); } } 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 d8315df38..85c37a643 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 @@ -10,7 +10,9 @@ 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; @@ -37,16 +39,21 @@ 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.ID; -import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE; 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.util.LogUtils.logException; import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @@ -70,9 +77,9 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { private final int maxLatency, maxIdleTime; private final AtomicBoolean used = new AtomicBoolean(false); - private volatile boolean running = false, contactConnections = false; + protected final PluginState state = new PluginState(); + private volatile String contactConnectionsUuid = null; - private volatile SS socket = null; abstract void initialiseAdapter() throws IOException; @@ -124,14 +131,18 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { LOG.info("Bluetooth enabled"); // We may not have been able to get the local address before ioExecutor.execute(this::updateProperties); - if (shouldAllowContactConnections()) bind(); + if (getState() == INACTIVE) bind(); } void onAdapterDisabled() { LOG.info("Bluetooth disabled"); - tryToClose(socket); connectionLimiter.allConnectionsClosed(); - callback.transportDisabled(); + // The server socket may not have been closed automatically + SS ss = state.clearServerSocket(); + if (ss != null) { + LOG.info("Closing server socket"); + tryToClose(ss); + } } @Override @@ -152,31 +163,24 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public void start() throws PluginException { if (used.getAndSet(true)) throw new IllegalStateException(); + Settings settings = callback.getSettings(); + boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE, false); + state.setStarted(enabledByUser); try { initialiseAdapter(); } catch (IOException e) { throw new PluginException(e); } updateProperties(); - running = true; - loadSettings(callback.getSettings()); - if (shouldAllowContactConnections()) { + if (enabledByUser) { if (isAdapterEnabled()) bind(); else enableAdapter(); } } - private void loadSettings(Settings settings) { - contactConnections = settings.getBoolean(PREF_BT_ENABLE, false); - } - - private boolean shouldAllowContactConnections() { - return contactConnections; - } - private void bind() { ioExecutor.execute(() -> { - if (!isRunning() || !shouldAllowContactConnections()) return; + if (getState() != INACTIVE) return; // Bind a server socket to accept connections from contacts SS ss; try { @@ -185,14 +189,13 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { logException(LOG, WARNING, e); return; } - if (!isRunning() || !shouldAllowContactConnections()) { + if (!state.setServerSocket(ss)) { + LOG.info("Closing redundant server socket"); tryToClose(ss); return; } - socket = ss; backoff.reset(); - callback.transportEnabled(); - acceptContactConnections(); + acceptContactConnections(ss); }); } @@ -221,35 +224,39 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { if (changed) callback.mergeLocalProperties(p); } - private void acceptContactConnections() { + private void acceptContactConnections(SS ss) { while (true) { DuplexTransportConnection conn; try { - conn = acceptConnection(socket); + conn = acceptConnection(ss); } catch (IOException e) { - // This is expected when the socket is closed - if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + // 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(); callback.handleConnection(conn); - if (!running) return; } } @Override public void stop() { - running = false; - tryToClose(socket); - callback.transportDisabled(); + SS ss = state.setStopped(); + tryToClose(ss); disableAdapterIfEnabledByUs(); } @Override - public boolean isRunning() { - return running && isAdapterEnabled(); + public State getState() { + return state.getState(); + } + + @Override + public int getReasonsDisabled() { + return state.getReasonsDisabled(); } @Override @@ -265,7 +272,7 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public void poll(Collection> properties) { - if (!isRunning() || !shouldAllowContactConnections()) return; + if (getState() != ACTIVE) return; backoff.increment(); for (Pair p : properties) { connect(p.getFirst(), p.getSecond()); @@ -319,7 +326,7 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public DuplexTransportConnection createConnection(TransportProperties p) { - if (!isRunning() || !shouldAllowContactConnections()) return null; + if (getState() != ACTIVE) return null; if (!connectionLimiter.canOpenContactConnection()) return null; String address = p.get(PROP_ADDRESS); if (isNullOrEmpty(address)) return null; @@ -337,7 +344,7 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public KeyAgreementListener createKeyAgreementListener(byte[] commitment) { - if (!isRunning()) return null; + 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); @@ -349,7 +356,7 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { logException(LOG, WARNING, e); return null; } - if (!isRunning()) { + if (getState() != ACTIVE) { tryToClose(ss); return null; } @@ -363,7 +370,7 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { @Override public DuplexTransportConnection createKeyAgreementConnection( byte[] commitment, BdfList descriptor) { - if (!isRunning()) return null; + if (getState() != ACTIVE) return null; // No truncation necessary because COMMIT_LENGTH = 16 String uuid = UUID.nameUUIDFromBytes(commitment).toString(); DuplexTransportConnection conn; @@ -423,17 +430,17 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { } } + @IoExecutor private void onSettingsUpdated(Settings settings) { - boolean wasAllowed = shouldAllowContactConnections(); - loadSettings(settings); - boolean isAllowed = shouldAllowContactConnections(); - if (wasAllowed && !isAllowed) { - LOG.info("Contact connections disabled"); - tryToClose(socket); - callback.transportDisabled(); + boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE, false); + SS ss = state.setEnabledByUser(enabledByUser); + State s = getState(); + if (ss != null) { + LOG.info("Disabled by user, closing server socket"); + tryToClose(ss); disableAdapterIfEnabledByUs(); - } else if (!wasAllowed && isAllowed) { - LOG.info("Contact connections enabled"); + } else if (s == INACTIVE) { + LOG.info("Enabled by user, opening server socket"); if (isAdapterEnabled()) bind(); else enableAdapter(); } @@ -461,4 +468,70 @@ abstract class BluetoothPlugin implements DuplexPlugin, EventListener { 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; + } + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java index 0d4f56618..8a2673a7a 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/file/FilePlugin.java @@ -16,6 +16,7 @@ import java.util.logging.Logger; import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.plugin.FileConstants.PROP_PATH; +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; @@ -45,7 +46,7 @@ abstract class FilePlugin implements SimplexPlugin { @Override public TransportConnectionReader createReader(TransportProperties p) { - if (!isRunning()) return null; + if (getState() != ACTIVE) return null; String path = p.get(PROP_PATH); if (isNullOrEmpty(path)) return null; try { @@ -60,7 +61,7 @@ abstract class FilePlugin implements SimplexPlugin { @Override public TransportConnectionWriter createWriter(TransportProperties p) { - if (!isRunning()) return null; + if (getState() != ACTIVE) return null; String path = p.get(PROP_PATH); if (isNullOrEmpty(path)) return null; try { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java index e955e871c..b7d64b3fa 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPlugin.java @@ -11,10 +11,10 @@ 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.settings.Settings; -import org.briarproject.bramble.util.IoUtils; import java.io.IOException; import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.InterfaceAddress; @@ -22,8 +22,9 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedList; import java.util.List; -import java.util.Random; import java.util.concurrent.Executor; import java.util.logging.Logger; @@ -31,38 +32,48 @@ import javax.annotation.Nullable; import static java.lang.Integer.parseInt; import static java.util.Collections.addAll; +import static java.util.Collections.emptyList; import static java.util.Collections.sort; 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_LAN; import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID; +import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_IPV6; import static org.briarproject.bramble.api.plugin.LanTcpConstants.PREF_LAN_IP_PORTS; +import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IPV6; import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_IP_PORTS; import static org.briarproject.bramble.api.plugin.LanTcpConstants.PROP_PORT; +import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED; +import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress; +import static org.briarproject.bramble.util.StringUtils.fromHexString; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; import static org.briarproject.bramble.util.StringUtils.join; +import static org.briarproject.bramble.util.StringUtils.toHexString; +import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; @NotNullByDefault class LanTcpPlugin extends TcpPlugin { private static final Logger LOG = getLogger(LanTcpPlugin.class.getName()); - private static final int MAX_ADDRESSES = 4; private static final String SEPARATOR = ","; /** * The IP address of an Android device providing a wifi access point. + *

+ * Most devices use this address, but at least one device (Honor 8A) may + * use other addresses in the range 192.168.43.0/24. */ - protected static final InetAddress WIFI_AP_ADDRESS; + private static final InetAddress WIFI_AP_ADDRESS; /** * The IP address of an Android device providing a wifi direct * legacy mode access point. */ - protected static final InetAddress WIFI_DIRECT_AP_ADDRESS; + private static final InetAddress WIFI_DIRECT_AP_ADDRESS; static { try { @@ -91,29 +102,30 @@ class LanTcpPlugin extends TcpPlugin { public void start() { if (used.getAndSet(true)) throw new IllegalStateException(); initialisePortProperty(); - running = true; + Settings settings = callback.getSettings(); + state.setStarted(settings.getBoolean(PREF_PLUGIN_ENABLE, false)); bind(); } protected void initialisePortProperty() { TransportProperties p = callback.getLocalProperties(); if (isNullOrEmpty(p.get(PROP_PORT))) { - int port = new Random().nextInt(32768) + 32768; + int port = chooseEphemeralPort(); p.put(PROP_PORT, String.valueOf(port)); callback.mergeLocalProperties(p); } } @Override - protected List getLocalSocketAddresses() { + protected List getLocalSocketAddresses(boolean ipv4) { TransportProperties p = callback.getLocalProperties(); int preferredPort = parsePortProperty(p.get(PROP_PORT)); String oldIpPorts = p.get(PROP_IP_PORTS); - List olds = parseSocketAddresses(oldIpPorts); + List olds = parseIpv4SocketAddresses(oldIpPorts); List locals = new ArrayList<>(); List fallbacks = new ArrayList<>(); - for (InetAddress local : getUsableLocalInetAddresses()) { + for (InetAddress local : getUsableLocalInetAddresses(ipv4)) { // If we've used this address before, try to use the same port int port = preferredPort; for (InetSocketAddress old : olds) { @@ -139,17 +151,17 @@ class LanTcpPlugin extends TcpPlugin { } } - private List parseSocketAddresses(String ipPorts) { + private List parseIpv4SocketAddresses(String ipPorts) { List addresses = new ArrayList<>(); if (isNullOrEmpty(ipPorts)) return addresses; for (String ipPort : ipPorts.split(SEPARATOR)) { - InetSocketAddress a = parseSocketAddress(ipPort); + InetSocketAddress a = parseIpv4SocketAddress(ipPort); if (a != null) addresses.add(a); } return addresses; } - protected List getUsableLocalInetAddresses() { + protected List getUsableLocalInetAddresses(boolean ipv4) { List ifAddrs = new ArrayList<>(getLocalInterfaceAddresses()); // Prefer longer network prefixes @@ -158,50 +170,74 @@ class LanTcpPlugin extends TcpPlugin { List addrs = new ArrayList<>(); for (InterfaceAddress ifAddr : ifAddrs) { InetAddress addr = ifAddr.getAddress(); - if (isAcceptableAddress(addr)) addrs.add(addr); + if (isAcceptableAddress(addr, ipv4)) addrs.add(addr); } return addrs; } @Override - protected void setLocalSocketAddress(InetSocketAddress a) { + protected void setLocalSocketAddress(InetSocketAddress a, boolean ipv4) { + if (ipv4) setLocalIpv4SocketAddress(a); + else setLocalIpv6SocketAddress(a); + } + + private void setLocalIpv4SocketAddress(InetSocketAddress a) { String ipPort = getIpPortString(a); + updateRecentAddresses(PREF_LAN_IP_PORTS, PROP_IP_PORTS, ipPort); + } + + private void setLocalIpv6SocketAddress(InetSocketAddress a) { + String hex = toHexString(a.getAddress().getAddress()); + updateRecentAddresses(PREF_IPV6, PROP_IPV6, hex); + } + + private void updateRecentAddresses(String settingKey, String propertyKey, + String item) { // Get the list of recently used addresses - String setting = callback.getSettings().get(PREF_LAN_IP_PORTS); - List recent = new ArrayList<>(); - if (!isNullOrEmpty(setting)) + String setting = callback.getSettings().get(settingKey); + Deque recent = new LinkedList<>(); + if (!isNullOrEmpty(setting)) { addAll(recent, setting.split(SEPARATOR)); - // Is the address already in the list? - if (recent.remove(ipPort)) { - // Move the address to the start of the list - recent.add(0, ipPort); + } + if (recent.remove(item)) { + // Move the item to the start of the list + recent.addFirst(item); setting = join(recent, SEPARATOR); } else { - // Add the address to the start of the list - recent.add(0, ipPort); - // Drop the least recently used address if the list is full - if (recent.size() > MAX_ADDRESSES) - recent = recent.subList(0, MAX_ADDRESSES); + // Add the item to the start of the list + recent.addFirst(item); + // Drop items from the end of the list if it's too long to encode setting = join(recent, SEPARATOR); + while (utf8IsTooLong(setting, MAX_PROPERTY_LENGTH)) { + recent.removeLast(); + setting = join(recent, SEPARATOR); + } // Update the list of addresses shared with contacts - List shared = new ArrayList<>(recent); - sort(shared); - String property = join(shared, SEPARATOR); TransportProperties properties = new TransportProperties(); - properties.put(PROP_IP_PORTS, property); + properties.put(propertyKey, setting); callback.mergeLocalProperties(properties); } // Save the setting Settings settings = new Settings(); - settings.put(PREF_LAN_IP_PORTS, setting); + settings.put(settingKey, setting); callback.mergeSettings(settings); } + protected boolean isIpv6LinkLocalAddress(InetAddress a) { + return a instanceof Inet6Address && a.isLinkLocalAddress(); + } + @Override protected List getRemoteSocketAddresses( + TransportProperties p, boolean ipv4) { + if (ipv4) return getRemoteIpv4SocketAddresses(p); + else return getRemoteIpv6SocketAddresses(p); + } + + private List getRemoteIpv4SocketAddresses( TransportProperties p) { String ipPorts = p.get(PROP_IP_PORTS); - List remotes = parseSocketAddresses(ipPorts); + List remotes = parseIpv4SocketAddresses(ipPorts); int port = parsePortProperty(p.get(PROP_PORT)); // If the contact has a preferred port, we can guess their IP:port when // they're providing a wifi access point @@ -216,20 +252,52 @@ class LanTcpPlugin extends TcpPlugin { return remotes; } - private boolean isAcceptableAddress(InetAddress a) { - // Accept link-local and site-local IPv4 addresses - boolean ipv4 = a instanceof Inet4Address; - boolean loop = a.isLoopbackAddress(); - boolean link = a.isLinkLocalAddress(); - boolean site = a.isSiteLocalAddress(); - return ipv4 && !loop && (link || site); + private List getRemoteIpv6SocketAddresses( + TransportProperties p) { + List addrs = parseIpv6Addresses(p.get(PROP_IPV6)); + int port = parsePortProperty(p.get(PROP_PORT)); + if (addrs.isEmpty() || port == 0) return emptyList(); + List remotes = new ArrayList<>(); + for (InetAddress addr : addrs) { + remotes.add(new InetSocketAddress(addr, port)); + } + return remotes; + } + + private List parseIpv6Addresses(String property) { + if (isNullOrEmpty(property)) return emptyList(); + try { + List addrs = new ArrayList<>(); + for (String hex : property.split(SEPARATOR)) { + byte[] ip = fromHexString(hex); + if (ip.length == 16) addrs.add(InetAddress.getByAddress(ip)); + } + return addrs; + } catch (IllegalArgumentException | UnknownHostException e) { + return emptyList(); + } + } + + private boolean isAcceptableAddress(InetAddress a, boolean ipv4) { + if (ipv4) { + // Accept link-local and site-local IPv4 addresses + boolean isIpv4 = a instanceof Inet4Address; + boolean link = a.isLinkLocalAddress(); + boolean site = a.isSiteLocalAddress(); + return isIpv4 && (link || site); + } else { + // Accept link-local IPv6 addresses + return isIpv6LinkLocalAddress(a); + } } @Override protected boolean isConnectable(InterfaceAddress local, InetSocketAddress remote) { if (remote.getPort() == 0) return false; - if (!isAcceptableAddress(remote.getAddress())) return false; + InetAddress remoteAddress = remote.getAddress(); + boolean ipv4 = local.getAddress() instanceof Inet4Address; + if (!isAcceptableAddress(remoteAddress, ipv4)) return false; // Try to determine whether the address is on the same LAN as us byte[] localIp = local.getAddress().getAddress(); byte[] remoteIp = remote.getAddress().getAddress(); @@ -271,10 +339,10 @@ class LanTcpPlugin extends TcpPlugin { } catch (IOException e) { if (LOG.isLoggable(INFO)) LOG.info("Failed to bind " + scrubSocketAddress(addr)); - tryToClose(ss); + tryToClose(ss, LOG, WARNING); } } - if (ss == null || !ss.isBound()) { + if (ss == null) { LOG.info("Could not bind server socket for key agreement"); return null; } @@ -287,11 +355,18 @@ class LanTcpPlugin extends TcpPlugin { return new LanKeyAgreementListener(descriptor, ss); } + private List getLocalSocketAddresses() { + List addrs = new ArrayList<>(); + addrs.addAll(getLocalSocketAddresses(true)); + addrs.addAll(getLocalSocketAddresses(false)); + return addrs; + } + @Override public DuplexTransportConnection createKeyAgreementConnection( byte[] commitment, BdfList descriptor) { - if (!isRunning()) return null; - ServerSocket ss = socket; + ServerSocket ss = state.getServerSocket(true); + if (ss == null) return null; InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress()); if (local == null) { LOG.warning("No interface for key agreement server socket"); @@ -363,7 +438,7 @@ class LanTcpPlugin extends TcpPlugin { @Override public void close() { - IoUtils.tryToClose(ss, LOG, WARNING); + tryToClose(ss, LOG, WARNING); } } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java index cfbdcea80..6528a6571 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginFactory.java @@ -1,5 +1,6 @@ package org.briarproject.bramble.plugin.tcp; +import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.Backoff; import org.briarproject.bramble.api.plugin.BackoffFactory; @@ -26,11 +27,13 @@ public class LanTcpPluginFactory implements DuplexPluginFactory { private static final double BACKOFF_BASE = 1.2; private final Executor ioExecutor; + private final EventBus eventBus; private final BackoffFactory backoffFactory; - public LanTcpPluginFactory(Executor ioExecutor, + public LanTcpPluginFactory(Executor ioExecutor, EventBus eventBus, BackoffFactory backoffFactory) { this.ioExecutor = ioExecutor; + this.eventBus = eventBus; this.backoffFactory = backoffFactory; } @@ -48,7 +51,9 @@ public class LanTcpPluginFactory implements DuplexPluginFactory { public DuplexPlugin createPlugin(PluginCallback callback) { Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, MAX_POLLING_INTERVAL, BACKOFF_BASE); - return new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY, + LanTcpPlugin plugin = new LanTcpPlugin(ioExecutor, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME, CONNECTION_TIMEOUT); + eventBus.addListener(plugin); + return plugin; } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/PortMapperImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/PortMapperImpl.java index 584510705..8b5adc876 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/PortMapperImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/PortMapperImpl.java @@ -54,11 +54,13 @@ class PortMapperImpl implements PortMapper { shutdownManager.addShutdownHook(() -> deleteMapping(port)); } String externalString = gateway.getExternalIPAddress(); - if (LOG.isLoggable(INFO)) - LOG.info( - "External address " + scrubInetAddress(externalString)); - if (externalString != null) + if (externalString == null) { + LOG.info("External address not available"); + } else { external = InetAddress.getByName(externalString); + if (LOG.isLoggable(INFO)) + LOG.info("External address " + scrubInetAddress(external)); + } } catch (IOException | SAXException e) { logException(LOG, WARNING, e); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java index 4f11054a0..522d1f0d2 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/TcpPlugin.java @@ -3,8 +3,12 @@ package org.briarproject.bramble.plugin.tcp; import org.briarproject.bramble.PoliteExecutor; 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.KeyAgreementListener; +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; @@ -14,7 +18,8 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.rendezvous.KeyMaterialSource; import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint; -import org.briarproject.bramble.util.IoUtils; +import org.briarproject.bramble.api.settings.Settings; +import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent; import java.io.IOException; import java.net.InetAddress; @@ -35,19 +40,26 @@ import java.util.logging.Logger; import java.util.regex.Pattern; import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; import static java.util.Collections.emptyList; import static java.util.Collections.list; 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.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.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.util.PrivacyUtils.scrubSocketAddress; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @MethodsNotNullByDefault @ParametersNotNullByDefault -abstract class TcpPlugin implements DuplexPlugin { +abstract class TcpPlugin implements DuplexPlugin, EventListener { private static final Logger LOG = getLogger(TcpPlugin.class.getName()); @@ -60,28 +72,28 @@ abstract class TcpPlugin implements DuplexPlugin { protected final int maxLatency, maxIdleTime; protected final int connectionTimeout, socketTimeout; protected final AtomicBoolean used = new AtomicBoolean(false); - - protected volatile boolean running = false; - protected volatile ServerSocket socket = null; + protected final PluginState state = new PluginState(); /** * Returns zero or more socket addresses on which the plugin should listen, * in order of preference. At most one of the addresses will be bound. */ - protected abstract List getLocalSocketAddresses(); + protected abstract List getLocalSocketAddresses( + boolean ipv4); /** * Adds the address on which the plugin is listening to the transport * properties. */ - protected abstract void setLocalSocketAddress(InetSocketAddress a); + protected abstract void setLocalSocketAddress(InetSocketAddress a, + boolean ipv4); /** * Returns zero or more socket addresses for connecting to a contact with * the given transport properties. */ protected abstract List getRemoteSocketAddresses( - TransportProperties p); + TransportProperties p, boolean ipv4); /** * Returns true if connections to the given address can be attempted. @@ -118,49 +130,49 @@ abstract class TcpPlugin implements DuplexPlugin { @Override public void start() { if (used.getAndSet(true)) throw new IllegalStateException(); - running = true; + Settings settings = callback.getSettings(); + state.setStarted(settings.getBoolean(PREF_PLUGIN_ENABLE, false)); bind(); } protected void bind() { bindExecutor.execute(() -> { - if (!running) return; - if (socket != null && !socket.isClosed()) return; - ServerSocket ss = null; - for (InetSocketAddress addr : getLocalSocketAddresses()) { - try { - ss = new ServerSocket(); - ss.bind(addr); - break; - } catch (IOException e) { - if (LOG.isLoggable(INFO)) - LOG.info("Failed to bind " + scrubSocketAddress(addr)); - tryToClose(ss); - } - } - if (ss == null || !ss.isBound()) { - LOG.info("Could not bind server socket"); - return; - } - if (!running) { - tryToClose(ss); - return; - } - socket = ss; - backoff.reset(); - InetSocketAddress local = - (InetSocketAddress) ss.getLocalSocketAddress(); - setLocalSocketAddress(local); - if (LOG.isLoggable(INFO)) - LOG.info("Listening on " + scrubSocketAddress(local)); - callback.transportEnabled(); - acceptContactConnections(); + if (getState() != INACTIVE) return; + bind(true); + bind(false); }); } - protected void tryToClose(@Nullable ServerSocket ss) { - IoUtils.tryToClose(ss, LOG, WARNING); - callback.transportDisabled(); + private void bind(boolean ipv4) { + ServerSocket ss = null; + for (InetSocketAddress addr : getLocalSocketAddresses(ipv4)) { + try { + ss = new ServerSocket(); + ss.bind(addr); + break; + } catch (IOException e) { + if (LOG.isLoggable(INFO)) + LOG.info("Failed to bind " + scrubSocketAddress(addr)); + tryToClose(ss, LOG, WARNING); + } + } + if (ss == null) { + LOG.info("Could not bind server socket"); + return; + } + if (!state.setServerSocket(ss, ipv4)) { + LOG.info("Closing redundant server socket"); + tryToClose(ss, LOG, WARNING); + return; + } + backoff.reset(); + InetSocketAddress local = + (InetSocketAddress) ss.getLocalSocketAddress(); + setLocalSocketAddress(local, ipv4); + if (LOG.isLoggable(INFO)) + LOG.info("Listening on " + scrubSocketAddress(local)); + ServerSocket finalSocket = ss; + ioExecutor.execute(() -> acceptContactConnections(finalSocket, ipv4)); } String getIpPortString(InetSocketAddress a) { @@ -170,20 +182,22 @@ abstract class TcpPlugin implements DuplexPlugin { return addr + ":" + a.getPort(); } - private void acceptContactConnections() { - while (isRunning()) { + private void acceptContactConnections(ServerSocket ss, boolean ipv4) { + while (true) { Socket s; try { - s = socket.accept(); + s = ss.accept(); s.setSoTimeout(socketTimeout); } catch (IOException e) { - // This is expected when the socket is closed - if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + // This is expected when the server socket is closed + LOG.info("Server socket closed"); + state.clearServerSocket(ss, ipv4); return; } - if (LOG.isLoggable(INFO)) + if (LOG.isLoggable(INFO)) { LOG.info("Connection from " + scrubSocketAddress(s.getRemoteSocketAddress())); + } backoff.reset(); callback.handleConnection(new TcpTransportConnection(this, s)); } @@ -191,13 +205,17 @@ abstract class TcpPlugin implements DuplexPlugin { @Override public void stop() { - running = false; - tryToClose(socket); + for (ServerSocket ss : state.setStopped()) tryToClose(ss, LOG, WARNING); } @Override - public boolean isRunning() { - return running && socket != null && !socket.isClosed(); + public State getState() { + return state.getState(); + } + + @Override + public int getReasonsDisabled() { + return state.getReasonsDisabled(); } @Override @@ -213,7 +231,7 @@ abstract class TcpPlugin implements DuplexPlugin { @Override public void poll(Collection> properties) { - if (!isRunning()) return; + if (getState() != ACTIVE) return; backoff.increment(); for (Pair p : properties) { connect(p.getFirst(), p.getSecond()); @@ -232,14 +250,22 @@ abstract class TcpPlugin implements DuplexPlugin { @Override public DuplexTransportConnection createConnection(TransportProperties p) { - if (!isRunning()) return null; - ServerSocket ss = socket; + DuplexTransportConnection c = createConnection(p, true); + if (c != null) return c; + return createConnection(p, false); + } + + @Nullable + private DuplexTransportConnection createConnection(TransportProperties p, + boolean ipv4) { + ServerSocket ss = state.getServerSocket(ipv4); + if (ss == null) return null; InterfaceAddress local = getLocalInterfaceAddress(ss.getInetAddress()); if (local == null) { LOG.warning("No interface for server socket"); return null; } - for (InetSocketAddress remote : getRemoteSocketAddresses(p)) { + for (InetSocketAddress remote : getRemoteSocketAddresses(p, ipv4)) { // Don't try to connect to our own address if (!canConnectToOwnAddress() && remote.getAddress().equals(ss.getInetAddress())) { @@ -264,9 +290,10 @@ abstract class TcpPlugin implements DuplexPlugin { LOG.info("Connected to " + scrubSocketAddress(remote)); return new TcpTransportConnection(this, s); } catch (IOException e) { - if (LOG.isLoggable(INFO)) + if (LOG.isLoggable(INFO)) { LOG.info("Could not connect to " + scrubSocketAddress(remote)); + } } } return null; @@ -289,8 +316,12 @@ abstract class TcpPlugin implements DuplexPlugin { return new Socket(); } + int chooseEphemeralPort() { + return 32768 + (int) (Math.random() * 32768); + } + @Nullable - InetSocketAddress parseSocketAddress(String ipPort) { + InetSocketAddress parseIpv4SocketAddress(String ipPort) { if (isNullOrEmpty(ipPort)) return null; String[] split = ipPort.split(":"); if (split.length != 2) return null; @@ -301,14 +332,7 @@ abstract class TcpPlugin implements DuplexPlugin { InetAddress a = InetAddress.getByName(addr); int p = Integer.parseInt(port); return new InetSocketAddress(a, p); - } catch (UnknownHostException e) { - if (LOG.isLoggable(WARNING)) - // not scrubbing to enable us to find the problem - LOG.warning("Invalid address: " + addr); - return null; - } catch (NumberFormatException e) { - if (LOG.isLoggable(WARNING)) - LOG.warning("Invalid port: " + port); + } catch (UnknownHostException | NumberFormatException e) { return null; } } @@ -366,4 +390,113 @@ abstract class TcpPlugin implements DuplexPlugin { return emptyList(); } } + + @Override + public void eventOccurred(Event e) { + if (e instanceof SettingsUpdatedEvent) { + SettingsUpdatedEvent s = (SettingsUpdatedEvent) e; + if (s.getNamespace().equals(getId().getString())) + ioExecutor.execute(() -> onSettingsUpdated(s.getSettings())); + } + } + + @IoExecutor + private void onSettingsUpdated(Settings settings) { + boolean enabledByUser = settings.getBoolean(PREF_PLUGIN_ENABLE, false); + List toClose = state.setEnabledByUser(enabledByUser); + State s = getState(); + if (!toClose.isEmpty()) { + LOG.info("Disabled by user, closing server sockets"); + for (ServerSocket ss : toClose) tryToClose(ss, LOG, WARNING); + } else if (s == INACTIVE) { + LOG.info("Enabled by user, opening server sockets"); + bind(); + } + } + + @ThreadSafe + @NotNullByDefault + protected class PluginState { + + @GuardedBy("this") + private boolean started = false, stopped = false, enabledByUser = false; + + @GuardedBy("this") + @Nullable + private ServerSocket serverSocketV4 = null, serverSocketV6 = null; + + synchronized void setStarted(boolean enabledByUser) { + started = true; + this.enabledByUser = enabledByUser; + callback.pluginStateChanged(getState()); + } + + synchronized List setStopped() { + stopped = true; + List toClose = clearServerSockets(); + callback.pluginStateChanged(getState()); + return toClose; + } + + @GuardedBy("this") + private List clearServerSockets() { + List toClose = new ArrayList<>(2); + if (serverSocketV4 != null) { + toClose.add(serverSocketV4); + serverSocketV4 = null; + } + if (serverSocketV6 != null) { + toClose.add(serverSocketV6); + serverSocketV6 = null; + } + return toClose; + } + + synchronized List setEnabledByUser( + boolean enabledByUser) { + this.enabledByUser = enabledByUser; + List toClose = enabledByUser + ? emptyList() : clearServerSockets(); + callback.pluginStateChanged(getState()); + return toClose; + } + + @Nullable + synchronized ServerSocket getServerSocket(boolean ipv4) { + return ipv4 ? serverSocketV4 : serverSocketV6; + } + + synchronized boolean setServerSocket(ServerSocket ss, boolean ipv4) { + if (stopped) return false; + if (ipv4) { + if (serverSocketV4 != null) return false; + serverSocketV4 = ss; + } else { + if (serverSocketV6 != null) return false; + serverSocketV6 = ss; + } + callback.pluginStateChanged(getState()); + return true; + } + + synchronized void clearServerSocket(ServerSocket ss, boolean ipv4) { + if (ipv4) { + if (serverSocketV4 == ss) serverSocketV4 = null; + } else { + if (serverSocketV6 == ss) serverSocketV6 = null; + } + callback.pluginStateChanged(getState()); + } + + synchronized State getState() { + if (!started || stopped) return STARTING_STOPPING; + if (!enabledByUser) return DISABLED; + if (serverSocketV4 != null || serverSocketV6 != null) return ACTIVE; + return INACTIVE; + } + + synchronized int getReasonsDisabled() { + return getState() == DISABLED ? REASON_USER : 0; + } + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java index 0f4643cf1..42f686ebc 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPlugin.java @@ -43,10 +43,11 @@ class WanTcpPlugin extends TcpPlugin { } @Override - protected List getLocalSocketAddresses() { + protected List getLocalSocketAddresses(boolean ipv4) { + if (!ipv4) return emptyList(); // Use the same address and port as last time if available TransportProperties p = callback.getLocalProperties(); - InetSocketAddress old = parseSocketAddress(p.get(PROP_IP_PORT)); + InetSocketAddress old = parseIpv4SocketAddress(p.get(PROP_IP_PORT)); List addrs = new LinkedList<>(); for (InetAddress a : getLocalInetAddresses()) { if (isAcceptableAddress(a)) { @@ -76,14 +77,11 @@ class WanTcpPlugin extends TcpPlugin { return ipv4 && !loop && !link && !site; } - private int chooseEphemeralPort() { - return 32768 + (int) (Math.random() * 32768); - } - @Override protected List getRemoteSocketAddresses( - TransportProperties p) { - InetSocketAddress parsed = parseSocketAddress(p.get(PROP_IP_PORT)); + TransportProperties p, boolean ipv4) { + if (!ipv4) return emptyList(); + InetSocketAddress parsed = parseIpv4SocketAddress(p.get(PROP_IP_PORT)); if (parsed == null) return emptyList(); return singletonList(parsed); } @@ -96,7 +94,8 @@ class WanTcpPlugin extends TcpPlugin { } @Override - protected void setLocalSocketAddress(InetSocketAddress a) { + protected void setLocalSocketAddress(InetSocketAddress a, boolean ipv4) { + if (!ipv4) throw new AssertionError(); if (mappingResult != null && mappingResult.isUsable()) { // Advertise the external address to contacts if (a.equals(mappingResult.getInternal())) { diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java index 079697319..db0f2c2a8 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tcp/WanTcpPluginFactory.java @@ -1,5 +1,6 @@ package org.briarproject.bramble.plugin.tcp; +import org.briarproject.bramble.api.event.EventBus; import org.briarproject.bramble.api.lifecycle.ShutdownManager; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.Backoff; @@ -27,12 +28,14 @@ public class WanTcpPluginFactory implements DuplexPluginFactory { private static final double BACKOFF_BASE = 1.2; private final Executor ioExecutor; + private final EventBus eventBus; private final BackoffFactory backoffFactory; private final ShutdownManager shutdownManager; - public WanTcpPluginFactory(Executor ioExecutor, + public WanTcpPluginFactory(Executor ioExecutor, EventBus eventBus, BackoffFactory backoffFactory, ShutdownManager shutdownManager) { this.ioExecutor = ioExecutor; + this.eventBus = eventBus; this.backoffFactory = backoffFactory; this.shutdownManager = shutdownManager; } @@ -51,8 +54,10 @@ public class WanTcpPluginFactory implements DuplexPluginFactory { public DuplexPlugin createPlugin(PluginCallback callback) { Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL, MAX_POLLING_INTERVAL, BACKOFF_BASE); - return new WanTcpPlugin(ioExecutor, backoff, + WanTcpPlugin plugin = new WanTcpPlugin(ioExecutor, backoff, new PortMapperImpl(shutdownManager), callback, MAX_LATENCY, MAX_IDLE_TIME, CONNECTION_TIMEOUT); + eventBus.addListener(plugin); + return plugin; } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java index 1e670de96..944399799 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/CircumventionProvider.java @@ -17,7 +17,7 @@ public interface CircumventionProvider { String[] BLOCKED = {"CN", "IR", "EG", "BY", "TR", "SY", "VE"}; /** - * Countries where obfs4 bridge connection are likely to work. + * Countries where obfs4 or meek bridge connections are likely to work. * Should be a subset of {@link #BLOCKED}. */ String[] BRIDGES = { "CN", "IR", "EG", "BY", "TR", "SY", "VE" }; diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java index e23a0da48..374f221af 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java @@ -15,6 +15,7 @@ import org.briarproject.bramble.api.network.NetworkManager; import org.briarproject.bramble.api.network.NetworkStatus; import org.briarproject.bramble.api.network.event.NetworkStatusEvent; 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; @@ -54,6 +55,9 @@ import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.zip.ZipInputStream; +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; import javax.net.SocketFactory; import static java.util.Arrays.asList; @@ -65,6 +69,11 @@ import static java.util.logging.Logger.getLogger; import static net.freehaven.tor.control.TorControlCommands.HS_ADDRESS; import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +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.ENABLING; +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.plugin.TorConstants.CONTROL_PORT; import static org.briarproject.bramble.api.plugin.TorConstants.ID; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE; @@ -76,6 +85,9 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHE import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_PORT; import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V2; import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3; +import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY; +import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED; +import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA; import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES; import static org.briarproject.bramble.util.IoUtils.copyAndClose; import static org.briarproject.bramble.util.IoUtils.tryToClose; @@ -113,16 +125,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private final int maxLatency, maxIdleTime, socketTimeout; private final File torDirectory, torFile, geoIpFile, obfs4File, configFile; private final File doneFile, cookieFile; - private final ConnectionStatus connectionStatus; private final AtomicBoolean used = new AtomicBoolean(false); - private volatile ServerSocket socket = null; + protected final PluginState state = new PluginState(); + private volatile Socket controlSocket = null; private volatile TorControlConnection controlConnection = null; private volatile Settings settings = null; - protected volatile boolean running = false; - protected abstract int getProcessId(); protected abstract long getLastUpdateTime(); @@ -159,7 +169,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { configFile = new File(torDirectory, "torrc"); doneFile = new File(torDirectory, "done"); cookieFile = new File(torDirectory, ".tor/control_auth_cookie"); - connectionStatus = new ConnectionStatus(); // Don't execute more than one connection status check at a time connectionStatusExecutor = new PoliteExecutor("TorPlugin", ioExecutor, 1); @@ -190,7 +199,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } } // Load the settings - settings = callback.getSettings(); + settings = migrateSettings(callback.getSettings()); // Install or update the assets if necessary if (!assetsAreUpToDate()) installAssets(); if (cookieFile.exists() && !cookieFile.delete()) @@ -258,7 +267,6 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { // Tell Tor to exit when the control connection is closed controlConnection.takeOwnership(); controlConnection.resetConf(singletonList(OWNER)); - running = true; // Register to receive events from the Tor process controlConnection.setEventHandler(this); controlConnection.setEvents(asList(EVENTS)); @@ -266,11 +274,12 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { String phase = controlConnection.getInfo("status/bootstrap-phase"); if (phase != null && phase.contains("PROGRESS=100")) { LOG.info("Tor has already bootstrapped"); - connectionStatus.setBootstrapped(); + state.setBootstrapped(); } } catch (IOException e) { throw new PluginException(e); } + state.setStarted(); // Check whether we're online updateConnectionStatus(networkManager.getNetworkStatus(), batteryManager.isCharging()); @@ -278,6 +287,18 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { bind(); } + // TODO: Remove after a reasonable migration period (added 2020-06-25) + private Settings migrateSettings(Settings settings) { + int network = settings.getInt(PREF_TOR_NETWORK, + PREF_TOR_NETWORK_AUTOMATIC); + if (network == PREF_TOR_NETWORK_NEVER) { + settings.putInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_AUTOMATIC); + settings.putBoolean(PREF_PLUGIN_ENABLE, false); + callback.mergeSettings(settings); + } + return settings; + } + private boolean assetsAreUpToDate() { return doneFile.lastModified() > getLastUpdateTime(); } @@ -393,11 +414,11 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { tryToClose(ss, LOG, WARNING); return; } - if (!running) { + if (!state.setServerSocket(ss)) { + LOG.info("Closing redundant server socket"); tryToClose(ss, LOG, WARNING); return; } - socket = ss; // Store the port number String localPort = String.valueOf(ss.getLocalPort()); Settings s = new Settings(); @@ -412,7 +433,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private void publishHiddenService(String port) { - if (!running) return; + if (!state.isTorRunning()) return; LOG.info("Creating hidden service"); String privKey = settings.get(HS_PRIVKEY); Map portLines = singletonMap(80, "127.0.0.1:" + port); @@ -450,14 +471,15 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private void acceptContactConnections(ServerSocket ss) { - while (running) { + while (true) { Socket s; try { s = ss.accept(); s.setSoTimeout(socketTimeout); } catch (IOException e) { - // This is expected when the socket is closed - if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + // This is expected when the server socket is closed + LOG.info("Server socket closed"); + state.clearServerSocket(ss); return; } LOG.info("Connection received"); @@ -467,10 +489,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } protected void enableNetwork(boolean enable) throws IOException { - if (!running) return; - connectionStatus.enableNetwork(enable); + state.enableNetwork(enable); controlConnection.setConf("DisableNetwork", enable ? "0" : "1"); - if (!enable) callback.transportDisabled(); } private void enableBridges(boolean enable, boolean needsMeek) @@ -494,9 +514,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public void stop() { - running = false; - tryToClose(socket, LOG, WARNING); - callback.transportDisabled(); + ServerSocket ss = state.setStopped(); + tryToClose(ss, LOG, WARNING); if (controlSocket != null && controlConnection != null) { try { LOG.info("Stopping Tor"); @@ -510,8 +529,13 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } @Override - public boolean isRunning() { - return running && connectionStatus.isConnected(); + public State getState() { + return state.getState(); + } + + @Override + public int getReasonsDisabled() { + return state.getReasonsDisabled(); } @Override @@ -527,7 +551,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public void poll(Collection> properties) { - if (!isRunning()) return; + if (getState() != ACTIVE) return; backoff.increment(); for (Pair p : properties) { connect(p.getFirst(), p.getSecond()); @@ -546,7 +570,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public DuplexTransportConnection createConnection(TransportProperties p) { - if (!isRunning()) return null; + if (getState() != ACTIVE) return null; String bestOnion = null; String onion2 = p.get(PROP_ONION_V2); String onion3 = p.get(PROP_ONION_V3); @@ -634,8 +658,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { new TorTransportConnection(this, s)); } } catch (IOException e) { - // This is expected when the socket is closed - if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + // This is expected when the server socket is closed + LOG.info("Rendezvous server socket closed"); } }); Map portLines = @@ -663,10 +687,9 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { @Override public void circuitStatus(String status, String id, String path) { if (status.equals("BUILT") && - connectionStatus.getAndSetCircuitBuilt()) { + state.getAndSetCircuitBuilt()) { LOG.info("First circuit built"); backoff.reset(); - if (isRunning()) callback.transportEnabled(); } } @@ -697,9 +720,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { public void message(String severity, String msg) { if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg); if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) { - connectionStatus.setBootstrapped(); + state.setBootstrapped(); backoff.reset(); - if (isRunning()) callback.transportEnabled(); } } @@ -736,7 +758,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private void disableNetwork() { connectionStatusExecutor.execute(() -> { try { - enableNetwork(false); + if (state.isTorRunning()) enableNetwork(false); } catch (IOException ex) { logException(LOG, WARNING, ex); } @@ -746,12 +768,14 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { private void updateConnectionStatus(NetworkStatus status, boolean charging) { connectionStatusExecutor.execute(() -> { - if (!running) return; + if (!state.isTorRunning()) return; boolean online = status.isConnected(); boolean wifi = status.isWifi(); String country = locationUtils.getCurrentCountry(); boolean blocked = circumventionProvider.isTorProbablyBlocked(country); + boolean enabledByUser = + settings.getBoolean(PREF_PLUGIN_ENABLE, true); int network = settings.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_AUTOMATIC); boolean useMobile = settings.getBoolean(PREF_TOR_MOBILE, true); @@ -762,47 +786,70 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { if (LOG.isLoggable(INFO)) { LOG.info("Online: " + online + ", wifi: " + wifi); - if ("".equals(country)) LOG.info("Country code unknown"); + if (country.isEmpty()) LOG.info("Country code unknown"); else LOG.info("Country code: " + country); LOG.info("Charging: " + charging); } - try { - if (!online) { - LOG.info("Disabling network, device is offline"); - enableNetwork(false); - } else if (!charging && onlyWhenCharging) { - LOG.info("Disabling network, device is on battery"); - enableNetwork(false); - } else if (network == PREF_TOR_NETWORK_NEVER || - (!useMobile && !wifi)) { - LOG.info("Disabling network, device is using mobile data"); - enableNetwork(false); - } else if (automatic && blocked && !bridgesWork) { - LOG.info("Disabling network, country is blocked"); - enableNetwork(false); - } else if (network == PREF_TOR_NETWORK_WITH_BRIDGES || - (automatic && bridgesWork)) { - if (circumventionProvider.needsMeek(country)) { - LOG.info("Enabling network, using meek bridges"); - enableBridges(true, true); + int reasonsDisabled = 0; + boolean enableNetwork = false, enableBridges = false; + boolean useMeek = false, enableConnectionPadding = false; + + if (!online) { + LOG.info("Disabling network, device is offline"); + } else { + if (!enabledByUser) { + LOG.info("User has disabled Tor"); + reasonsDisabled |= REASON_USER; + } + if (!charging && onlyWhenCharging) { + LOG.info("Configured not to use battery"); + reasonsDisabled |= REASON_BATTERY; + } + if (!useMobile && !wifi) { + LOG.info("Configured not to use mobile data"); + reasonsDisabled |= REASON_MOBILE_DATA; + } + if (automatic && blocked && !bridgesWork) { + LOG.info("Country is blocked"); + reasonsDisabled |= REASON_COUNTRY_BLOCKED; + } + + if (reasonsDisabled != 0) { + LOG.info("Disabling network due to settings"); + } else { + LOG.info("Enabling network"); + enableNetwork = true; + if (network == PREF_TOR_NETWORK_WITH_BRIDGES || + (automatic && bridgesWork)) { + if (circumventionProvider.needsMeek(country)) { + LOG.info("Using meek bridges"); + enableBridges = true; + useMeek = true; + } else { + LOG.info("Using obfs4 bridges"); + enableBridges = true; + } } else { - LOG.info("Enabling network, using obfs4 bridges"); - enableBridges(true, false); + LOG.info("Not using bridges"); + } + if (wifi && charging) { + LOG.info("Enabling connection padding"); + enableConnectionPadding = true; + } else { + LOG.info("Disabling connection padding"); } - enableNetwork(true); - } else { - LOG.info("Enabling network, not using bridges"); - enableBridges(false, false); - enableNetwork(true); } - if (online && wifi && charging) { - LOG.info("Enabling connection padding"); - enableConnectionPadding(true); - } else { - LOG.info("Disabling connection padding"); - enableConnectionPadding(false); + } + + state.setReasonsDisabled(reasonsDisabled); + + try { + if (enableNetwork) { + enableBridges(enableBridges, useMeek); + enableConnectionPadding(enableConnectionPadding); } + enableNetwork(enableNetwork); } catch (IOException e) { logException(LOG, WARNING, e); } @@ -810,33 +857,96 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener { } private void enableConnectionPadding(boolean enable) throws IOException { - if (!running) return; controlConnection.setConf("ConnectionPadding", enable ? "1" : "0"); } - private static class ConnectionStatus { + @ThreadSafe + @NotNullByDefault + protected class PluginState { - // All of the following are locking: this - private boolean networkEnabled = false; - private boolean bootstrapped = false, circuitBuilt = false; + @GuardedBy("this") + private boolean started = false, + stopped = false, + networkInitialised = false, + networkEnabled = false, + bootstrapped = false, + circuitBuilt = false, + settingsChecked = false; - private synchronized void setBootstrapped() { - bootstrapped = true; + @GuardedBy("this") + private int reasonsDisabled = 0; + + @GuardedBy("this") + @Nullable + private ServerSocket serverSocket = null; + + synchronized void setStarted() { + started = true; + callback.pluginStateChanged(getState()); } - private synchronized boolean getAndSetCircuitBuilt() { + synchronized boolean isTorRunning() { + return started && !stopped; + } + + @Nullable + synchronized ServerSocket setStopped() { + stopped = true; + ServerSocket ss = serverSocket; + serverSocket = null; + callback.pluginStateChanged(getState()); + return ss; + } + + synchronized void setBootstrapped() { + bootstrapped = true; + callback.pluginStateChanged(getState()); + } + + synchronized boolean getAndSetCircuitBuilt() { boolean firstCircuit = !circuitBuilt; circuitBuilt = true; + callback.pluginStateChanged(getState()); return firstCircuit; } - private synchronized void enableNetwork(boolean enable) { + synchronized void enableNetwork(boolean enable) { + networkInitialised = true; networkEnabled = enable; if (!enable) circuitBuilt = false; + callback.pluginStateChanged(getState()); } - private synchronized boolean isConnected() { - return networkEnabled && bootstrapped && circuitBuilt; + synchronized void setReasonsDisabled(int reasonsDisabled) { + settingsChecked = true; + this.reasonsDisabled = reasonsDisabled; + callback.pluginStateChanged(getState()); + } + + // Doesn't affect getState() + synchronized boolean setServerSocket(ServerSocket ss) { + if (stopped || serverSocket != null) return false; + serverSocket = ss; + return true; + } + + // Doesn't affect getState() + synchronized void clearServerSocket(ServerSocket ss) { + if (serverSocket == ss) serverSocket = null; + } + + synchronized State getState() { + if (!started || stopped || !settingsChecked) { + return STARTING_STOPPING; + } + if (reasonsDisabled != 0) return DISABLED; + if (!networkInitialised) return ENABLING; + if (!networkEnabled) return INACTIVE; + return bootstrapped && circuitBuilt ? ACTIVE : ENABLING; + } + + synchronized int getReasonsDisabled() { + return getState() == DISABLED ? reasonsDisabled : 0; } } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java index 8e41e9b64..9f6a0cb24 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java @@ -31,8 +31,8 @@ import org.briarproject.bramble.api.plugin.TransportConnectionWriter; 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.plugin.event.TransportDisabledEvent; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.rendezvous.KeyMaterialSource; import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint; @@ -269,11 +269,11 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener { } else if (e instanceof PendingContactRemovedEvent) { PendingContactRemovedEvent p = (PendingContactRemovedEvent) e; removePendingContactAsync(p.getId()); - } else if (e instanceof TransportEnabledEvent) { - TransportEnabledEvent t = (TransportEnabledEvent) e; + } else if (e instanceof TransportActiveEvent) { + TransportActiveEvent t = (TransportActiveEvent) e; addTransportAsync(t.getTransportId()); - } else if (e instanceof TransportDisabledEvent) { - TransportDisabledEvent t = (TransportDisabledEvent) e; + } else if (e instanceof TransportInactiveEvent) { + TransportInactiveEvent t = (TransportInactiveEvent) e; removeTransportAsync(t.getTransportId()); } else if (e instanceof RendezvousConnectionOpenedEvent) { RendezvousConnectionOpenedEvent r = diff --git a/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java index f03d4f730..228569fa2 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/reporting/DevReporterImpl.java @@ -6,7 +6,7 @@ import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TorConstants; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; import org.briarproject.bramble.api.reporting.DevConfig; import org.briarproject.bramble.api.reporting.DevReporter; import org.briarproject.bramble.util.IoUtils; @@ -92,8 +92,8 @@ class DevReporterImpl implements DevReporter, EventListener { @Override public void eventOccurred(Event e) { - if (e instanceof TransportEnabledEvent) { - TransportEnabledEvent t = (TransportEnabledEvent) e; + if (e instanceof TransportActiveEvent) { + TransportActiveEvent t = (TransportActiveEvent) e; if (t.getTransportId().equals(TorConstants.ID)) ioExecutor.execute(this::sendReports); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java index a74302d62..6eed42511 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/DuplexOutgoingSession.java @@ -12,6 +12,7 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Offer; @@ -238,6 +239,9 @@ class DuplexOutgoingSession implements SyncSession, EventListener { } else if (e instanceof CloseSyncConnectionsEvent) { CloseSyncConnectionsEvent c = (CloseSyncConnectionsEvent) e; if (c.getTransportId().equals(transportId)) interrupt(); + } else if (e instanceof TransportInactiveEvent) { + TransportInactiveEvent t = (TransportInactiveEvent) e; + if (t.getTransportId().equals(transportId)) interrupt(); } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java b/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java index 2697a2cfc..1d32ca4ee 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/sync/SimplexOutgoingSession.java @@ -12,6 +12,7 @@ import org.briarproject.bramble.api.lifecycle.IoExecutor; import org.briarproject.bramble.api.lifecycle.event.LifecycleEvent; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.sync.Ack; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.SyncRecordWriter; @@ -131,6 +132,9 @@ class SimplexOutgoingSession implements SyncSession, EventListener { } else if (e instanceof CloseSyncConnectionsEvent) { CloseSyncConnectionsEvent c = (CloseSyncConnectionsEvent) e; if (c.getTransportId().equals(transportId)) interrupt(); + } else if (e instanceof TransportInactiveEvent) { + TransportInactiveEvent t = (TransportInactiveEvent) e; + if (t.getTransportId().equals(transportId)) interrupt(); } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/PollerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/PollerImplTest.java index cf29a7a0e..5c65311e0 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/plugin/PollerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/PollerImplTest.java @@ -13,8 +13,8 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.event.ConnectionClosedEvent; import org.briarproject.bramble.api.plugin.event.ConnectionOpenedEvent; -import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportPropertyManager; @@ -331,7 +331,7 @@ public class PollerImplTest extends BrambleMockTestCase { } @Test - public void testPollsOnTransportEnabled() throws Exception { + public void testPollsOnTransportActivated() throws Exception { DuplexPlugin plugin = context.mock(DuplexPlugin.class); context.checking(new Expectations() {{ @@ -370,7 +370,7 @@ public class PollerImplTest extends BrambleMockTestCase { pairOf(equal(properties), any(ConnectionHandler.class))))); }}); - poller.eventOccurred(new TransportEnabledEvent(transportId)); + poller.eventOccurred(new TransportActiveEvent(transportId)); } @Test @@ -411,11 +411,11 @@ public class PollerImplTest extends BrambleMockTestCase { // All contacts are connected, so don't poll the plugin }}); - poller.eventOccurred(new TransportEnabledEvent(transportId)); + poller.eventOccurred(new TransportActiveEvent(transportId)); } @Test - public void testCancelsPollingOnTransportDisabled() { + public void testCancelsPollingOnTransportDeactivated() { Plugin plugin = context.mock(Plugin.class); context.checking(new Expectations() {{ @@ -433,12 +433,12 @@ public class PollerImplTest extends BrambleMockTestCase { oneOf(scheduler).schedule(with(any(Runnable.class)), with(0L), with(MILLISECONDS)); will(returnValue(future)); - // The plugin is disabled before the task runs - cancel the task + // The plugin is deactivated before the task runs - cancel the task oneOf(future).cancel(false); }}); - poller.eventOccurred(new TransportEnabledEvent(transportId)); - poller.eventOccurred(new TransportDisabledEvent(transportId)); + poller.eventOccurred(new TransportActiveEvent(transportId)); + poller.eventOccurred(new TransportInactiveEvent(transportId)); } private void expectReschedule(Plugin plugin) { diff --git a/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java b/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java index 73a2f7151..1ae844f7b 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/plugin/tcp/LanTcpPluginTest.java @@ -4,6 +4,7 @@ import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.keyagreement.KeyAgreementListener; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.Backoff; +import org.briarproject.bramble.api.plugin.Plugin.State; import org.briarproject.bramble.api.plugin.PluginCallback; import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportConnectionWriter; @@ -31,6 +32,7 @@ import static java.util.concurrent.Executors.newCachedThreadPool; import static java.util.concurrent.TimeUnit.SECONDS; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH; import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN; +import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE; import static org.briarproject.bramble.plugin.tcp.LanTcpPlugin.areAddressesInSameNetwork; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -302,10 +304,15 @@ public class LanTcpPluginTest extends BrambleTestCase { private final CountDownLatch propertiesLatch = new CountDownLatch(2); private final CountDownLatch connectionsLatch = new CountDownLatch(1); private final TransportProperties local = new TransportProperties(); + private final Settings settings = new Settings(); + + private Callback() { + settings.putBoolean(PREF_PLUGIN_ENABLE, true); + } @Override public Settings getSettings() { - return new Settings(); + return settings; } @Override @@ -324,11 +331,7 @@ public class LanTcpPluginTest extends BrambleTestCase { } @Override - public void transportEnabled() { - } - - @Override - public void transportDisabled() { + public void pluginStateChanged(State newState) { } @Override diff --git a/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java index eddaacce4..42f522e4c 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java @@ -17,8 +17,8 @@ import org.briarproject.bramble.api.plugin.ConnectionHandler; import org.briarproject.bramble.api.plugin.PluginManager; import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin; -import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportActiveEvent; +import org.briarproject.bramble.api.plugin.event.TransportInactiveEvent; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.rendezvous.KeyMaterialSource; import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint; @@ -178,10 +178,10 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { rendezvousPoller.startService(); context.assertIsSatisfied(); - // Enable the transport - no endpoints should be created yet + // Activate the transport - no endpoints should be created yet expectGetPlugin(); - rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId)); + rendezvousPoller.eventOccurred(new TransportActiveEvent(transportId)); context.assertIsSatisfied(); // Add the pending contact - endpoint should be created and polled @@ -212,8 +212,8 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { new PendingContactRemovedEvent(pendingContact.getId())); context.assertIsSatisfied(); - // Disable the transport - endpoint is already closed - rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId)); + // Deactivate the transport - endpoint is already closed + rendezvousPoller.eventOccurred(new TransportInactiveEvent(transportId)); } @Test @@ -230,10 +230,10 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { rendezvousPoller.startService(); context.assertIsSatisfied(); - // Enable the transport - no endpoints should be created yet + // Activate the transport - no endpoints should be created yet expectGetPlugin(); - rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId)); + rendezvousPoller.eventOccurred(new TransportActiveEvent(transportId)); context.assertIsSatisfied(); // Add the pending contact - endpoint should be created and polled @@ -269,12 +269,12 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { new PendingContactRemovedEvent(pendingContact.getId())); context.assertIsSatisfied(); - // Disable the transport - endpoint is already closed - rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId)); + // Deactivate the transport - endpoint is already closed + rendezvousPoller.eventOccurred(new TransportInactiveEvent(transportId)); } @Test - public void testCreatesAndClosesEndpointsWhenTransportIsEnabledAndDisabled() + public void testCreatesAndClosesEndpointsWhenTransportIsActivatedAndDeactivated() throws Exception { long beforeExpiry = pendingContact.getTimestamp(); @@ -292,19 +292,19 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase { new PendingContactAddedEvent(pendingContact)); context.assertIsSatisfied(); - // Enable the transport - endpoint should be created + // Activate the transport - endpoint should be created expectGetPlugin(); expectCreateEndpoint(); expectStateChangedEvent(WAITING_FOR_CONNECTION); - rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId)); + rendezvousPoller.eventOccurred(new TransportActiveEvent(transportId)); context.assertIsSatisfied(); - // Disable the transport - endpoint should be closed + // Deactivate the transport - endpoint should be closed expectCloseEndpoint(); expectStateChangedEvent(OFFLINE); - rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId)); + rendezvousPoller.eventOccurred(new TransportInactiveEvent(transportId)); context.assertIsSatisfied(); // Remove the pending contact - endpoint is already closed diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java index 53578ccc4..f7a5168b9 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/DesktopPluginModule.java @@ -45,9 +45,9 @@ public class DesktopPluginModule extends PluginModule { ioExecutor, random, eventBus, timeoutMonitor, backoffFactory); DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor, reliabilityFactory); - DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor, + DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor, eventBus, backoffFactory); - DuplexPluginFactory wan = new WanTcpPluginFactory(ioExecutor, + DuplexPluginFactory wan = new WanTcpPluginFactory(ioExecutor, eventBus, backoffFactory, shutdownManager); Collection duplex = asList(bluetooth, modem, lan, wan); diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java index 16a0d0f31..53a668f20 100644 --- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java +++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/modem/ModemPlugin.java @@ -4,6 +4,7 @@ import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.keyagreement.KeyAgreementListener; 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.ConnectionHandler; import org.briarproject.bramble.api.plugin.PluginCallback; @@ -23,9 +24,16 @@ import java.util.Collection; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; +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.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING; +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.util.LogUtils.logException; import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty; @@ -44,8 +52,8 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { private final PluginCallback callback; private final int maxLatency; private final AtomicBoolean used = new AtomicBoolean(false); + private final PluginState state = new PluginState(); - private volatile boolean running = false; private volatile Modem modem = null; ModemPlugin(ModemFactory modemFactory, SerialPortList serialPortList, @@ -75,6 +83,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { @Override public void start() throws PluginException { if (used.getAndSet(true)) throw new IllegalStateException(); + state.setStarted(); for (String portName : serialPortList.getPortNames()) { if (LOG.isLoggable(INFO)) LOG.info("Trying to initialise modem on " + portName); @@ -83,18 +92,20 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { if (!modem.start()) continue; if (LOG.isLoggable(INFO)) LOG.info("Initialised modem on " + portName); - running = true; + state.setInitialised(); return; } catch (IOException e) { logException(LOG, WARNING, e); } } + LOG.warning("Failed to initialised modem"); + state.setFailed(); throw new PluginException(); } @Override public void stop() { - running = false; + state.setStopped(); if (modem != null) { try { modem.stop(); @@ -105,8 +116,13 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { } @Override - public boolean isRunning() { - return running; + public State getState() { + return state.getState(); + } + + @Override + public int getReasonsDisabled() { + return 0; } @Override @@ -125,8 +141,8 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { throw new UnsupportedOperationException(); } - private boolean resetModem() { - if (!running) return false; + private void resetModem() { + if (getState() != ACTIVE) return; for (String portName : serialPortList.getPortNames()) { if (LOG.isLoggable(INFO)) LOG.info("Trying to initialise modem on " + portName); @@ -135,18 +151,18 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { if (!modem.start()) continue; if (LOG.isLoggable(INFO)) LOG.info("Initialised modem on " + portName); - return true; + return; } catch (IOException e) { logException(LOG, WARNING, e); } } - running = false; - return false; + LOG.warning("Failed to initialise modem"); + state.setFailed(); } @Override public DuplexTransportConnection createConnection(TransportProperties p) { - if (!running) return null; + if (getState() != ACTIVE) return null; // Get the ISO 3166 code for the caller's country String fromIso = callback.getLocalProperties().get("iso3166"); if (isNullOrEmpty(fromIso)) return null; @@ -232,4 +248,41 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback { if (exception) resetModem(); } } + + @ThreadSafe + @NotNullByDefault + private class PluginState { + + @GuardedBy("this") + private boolean started = false, + stopped = false, + initialised = false, + failed = false; + + private synchronized void setStarted() { + started = true; + callback.pluginStateChanged(getState()); + } + + private synchronized void setStopped() { + stopped = true; + callback.pluginStateChanged(getState()); + } + + private synchronized void setInitialised() { + initialised = true; + callback.pluginStateChanged(getState()); + } + + private synchronized void setFailed() { + failed = true; + callback.pluginStateChanged(getState()); + } + + private State getState() { + if (!started || stopped) return STARTING_STOPPING; + if (failed) return INACTIVE; + return initialised ? ACTIVE : ENABLING; + } + } } diff --git a/bramble-java/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java b/bramble-java/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java index df03d3abf..4e3ae0684 100644 --- a/bramble-java/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java +++ b/bramble-java/src/test/java/org/briarproject/bramble/plugin/modem/ModemPluginTest.java @@ -9,6 +9,8 @@ import org.junit.Test; import java.io.IOException; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -33,6 +35,7 @@ public class ModemPluginTest extends BrambleMockTestCase { @Test public void testModemCreation() throws Exception { context.checking(new Expectations() {{ + oneOf(callback).pluginStateChanged(ENABLING); oneOf(serialPortList).getPortNames(); will(returnValue(new String[] {"foo", "bar", "baz"})); // First call to createModem() returns false @@ -50,6 +53,7 @@ public class ModemPluginTest extends BrambleMockTestCase { will(returnValue(modem)); oneOf(modem).start(); will(returnValue(true)); + oneOf(callback).pluginStateChanged(ACTIVE); }}); plugin.start(); @@ -65,12 +69,14 @@ public class ModemPluginTest extends BrambleMockTestCase { context.checking(new Expectations() {{ // start() + oneOf(callback).pluginStateChanged(ENABLING); oneOf(serialPortList).getPortNames(); will(returnValue(new String[] {"foo"})); oneOf(modemFactory).createModem(plugin, "foo"); will(returnValue(modem)); oneOf(modem).start(); will(returnValue(true)); + oneOf(callback).pluginStateChanged(ACTIVE); // createConnection() oneOf(callback).getLocalProperties(); will(returnValue(local)); @@ -93,12 +99,14 @@ public class ModemPluginTest extends BrambleMockTestCase { context.checking(new Expectations() {{ // start() + oneOf(callback).pluginStateChanged(ENABLING); oneOf(serialPortList).getPortNames(); will(returnValue(new String[] {"foo"})); oneOf(modemFactory).createModem(plugin, "foo"); will(returnValue(modem)); oneOf(modem).start(); will(returnValue(true)); + oneOf(callback).pluginStateChanged(ACTIVE); // createConnection() oneOf(callback).getLocalProperties(); will(returnValue(local)); @@ -121,12 +129,14 @@ public class ModemPluginTest extends BrambleMockTestCase { context.checking(new Expectations() {{ // start() + oneOf(callback).pluginStateChanged(ENABLING); oneOf(serialPortList).getPortNames(); will(returnValue(new String[] {"foo"})); oneOf(modemFactory).createModem(plugin, "foo"); will(returnValue(modem)); oneOf(modem).start(); will(returnValue(true)); + oneOf(callback).pluginStateChanged(ACTIVE); // createConnection() oneOf(callback).getLocalProperties(); will(returnValue(local)); diff --git a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java index b0f47bd86..7439321fd 100644 --- a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java +++ b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/BridgeTest.java @@ -32,6 +32,7 @@ import javax.net.SocketFactory; import static java.util.Collections.singletonList; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; import static org.briarproject.bramble.test.TestUtils.getTestDirectory; import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled; @@ -141,10 +142,10 @@ public class BridgeTest extends BrambleTestCase { plugin.start(); long start = clock.currentTimeMillis(); while (clock.currentTimeMillis() - start < TIMEOUT) { - if (plugin.isRunning()) return; + if (plugin.getState() == ACTIVE) return; clock.sleep(500); } - if (!plugin.isRunning()) { + if (plugin.getState() != ACTIVE) { fail("Could not connect to Tor within timeout."); } } finally { diff --git a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/TestPluginCallback.java b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/TestPluginCallback.java index e8b8121da..aeb5a9ee9 100644 --- a/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/TestPluginCallback.java +++ b/bramble-java/src/test/java/org/briarproject/bramble/plugin/tor/TestPluginCallback.java @@ -1,6 +1,7 @@ package org.briarproject.bramble.plugin.tor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.Plugin.State; import org.briarproject.bramble.api.plugin.PluginCallback; import org.briarproject.bramble.api.plugin.TransportConnectionReader; import org.briarproject.bramble.api.plugin.TransportConnectionWriter; @@ -30,11 +31,7 @@ public class TestPluginCallback implements PluginCallback { } @Override - public void transportEnabled() { - } - - @Override - public void transportDisabled() { + public void pluginStateChanged(State state) { } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index 7231f9260..85476b1b8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -41,6 +41,7 @@ import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.keyagreement.ContactExchangeModule; import org.briarproject.briar.android.login.LoginModule; +import org.briarproject.briar.android.navdrawer.NavDrawerModule; import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.DozeWatchdog; @@ -76,6 +77,7 @@ import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; @Module(includes = { ContactExchangeModule.class, LoginModule.class, + NavDrawerModule.class, ViewModelModule.class }) public class AppModule { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java index e9ed9686c..5d559ec47 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java @@ -8,8 +8,6 @@ import org.briarproject.briar.android.controller.BriarController; import org.briarproject.briar.android.controller.BriarControllerImpl; import org.briarproject.briar.android.controller.DbController; import org.briarproject.briar.android.controller.DbControllerImpl; -import org.briarproject.briar.android.navdrawer.NavDrawerController; -import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl; import dagger.Module; import dagger.Provides; @@ -58,14 +56,6 @@ public class ActivityModule { return dbController; } - @ActivityScope - @Provides - NavDrawerController provideNavDrawerController( - NavDrawerControllerImpl navDrawerController) { - activity.addLifecycleController(navDrawerController); - return navDrawerController; - } - @ActivityScope @Provides BriarServiceConnection provideBriarServiceConnection() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java index 634c45462..aec20a01d 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/NicknameFragment.java @@ -3,7 +3,6 @@ package org.briarproject.briar.android.contact.add.remote; import android.content.Context; import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.Editable; import android.view.LayoutInflater; @@ -38,12 +37,10 @@ import androidx.lifecycle.ViewModelProviders; import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_LONG; -import static androidx.core.content.ContextCompat.getColor; -import static androidx.core.content.ContextCompat.getDrawable; -import static androidx.core.graphics.drawable.DrawableCompat.setTint; import static java.util.Objects.requireNonNull; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong; +import static org.briarproject.briar.android.util.UiUtils.getDialogIcon; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -199,9 +196,7 @@ public class NicknameFragment extends BaseFragment { private void showWarningDialog(String name1, String name2) { Context ctx = requireContext(); Builder b = new Builder(ctx, R.style.BriarDialogTheme); - Drawable icon = getDrawable(ctx, R.drawable.alerts_and_states_error); - setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary)); - b.setIcon(icon); + b.setIcon(getDialogIcon(ctx, R.drawable.alerts_and_states_error)); b.setTitle(getString(R.string.duplicate_link_dialog_title)); b.setMessage( getString(R.string.duplicate_link_dialog_text_3, name1, name2)); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index 3d4080f03..46468c030 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -230,7 +230,11 @@ class ConversationVisitor implements R.layout.list_item_conversation_notice_out, text, r); } else { String text; - if (r.isContact()) { + if (r.wasAnswered()) { + text = ctx.getString( + R.string.introduction_request_answered_received, + contactName.getValue(), name); + } else if (r.isContact()) { text = ctx.getString( R.string.introduction_request_exists_received, contactName.getValue(), name); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java index 749ef495c..10d2da314 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ImageActivity.java @@ -2,7 +2,6 @@ package org.briarproject.briar.android.conversation; import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.graphics.drawable.Drawable; import android.os.Bundle; import android.transition.Fade; import android.transition.Transition; @@ -35,8 +34,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; -import androidx.core.graphics.drawable.DrawableCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapter; @@ -59,6 +56,7 @@ import static com.google.android.material.snackbar.Snackbar.LENGTH_LONG; import static java.util.Objects.requireNonNull; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_SAVE_ATTACHMENT; import static org.briarproject.briar.android.util.UiUtils.formatDateAbsolute; +import static org.briarproject.briar.android.util.UiUtils.getDialogIcon; @MethodsNotNullByDefault @ParametersNotNullByDefault @@ -278,10 +276,7 @@ public class ImageActivity extends BriarActivity Builder builder = new Builder(this, R.style.BriarDialogTheme); builder.setTitle(getString(R.string.dialog_title_save_image)); builder.setMessage(getString(R.string.dialog_message_save_image)); - Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_security); - DrawableCompat.setTint(requireNonNull(icon), - ContextCompat.getColor(this, R.color.color_primary)); - builder.setIcon(icon); + builder.setIcon(getDialogIcon(this, R.drawable.ic_security)); builder.setPositiveButton(R.string.save_image, okListener); builder.setNegativeButton(R.string.cancel, null); builder.show(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java index 062f12059..f18c41f75 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeActivity.java @@ -9,6 +9,7 @@ import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; +import org.briarproject.briar.android.fragment.BaseFragment; import javax.annotation.Nullable; import javax.inject.Inject; @@ -79,13 +80,13 @@ public class ContactExchangeActivity extends KeyAgreementActivity { @UiThread private void contactExchangeFailed() { - showErrorFragment(R.string.connection_error_explanation); + showErrorFragment(); } @UiThread @Override public void keyAgreementFailed() { - showErrorFragment(R.string.connection_error_explanation); + showErrorFragment(); } @UiThread @@ -103,7 +104,7 @@ public class ContactExchangeActivity extends KeyAgreementActivity { @UiThread @Override public void keyAgreementAborted(boolean remoteAborted) { - showErrorFragment(R.string.connection_error_explanation); + showErrorFragment(); } @UiThread @@ -112,4 +113,10 @@ public class ContactExchangeActivity extends KeyAgreementActivity { startContactExchange(result); return getString(R.string.exchanging_contact_details); } + + protected void showErrorFragment() { + String errorMsg = getString(R.string.connection_error_explanation); + BaseFragment f = ContactExchangeErrorFragment.newInstance(errorMsg); + showNextFragment(f); + } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java index 16d8e2a03..f55d89a49 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/KeyAgreementActivity.java @@ -8,10 +8,18 @@ import android.content.IntentFilter; import android.os.Bundle; import android.view.MenuItem; +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.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.plugin.BluetoothConstants; +import org.briarproject.bramble.api.plugin.LanTcpConstants; +import org.briarproject.bramble.api.plugin.Plugin; +import org.briarproject.bramble.api.plugin.Plugin.State; +import org.briarproject.bramble.api.plugin.PluginManager; import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent; +import org.briarproject.bramble.api.plugin.event.TransportStateEvent; import org.briarproject.briar.R; import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; @@ -37,13 +45,15 @@ import static android.Manifest.permission.ACCESS_COARSE_LOCATION; import static android.Manifest.permission.CAMERA; import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE; import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED; -import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED; -import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE; -import static android.bluetooth.BluetoothAdapter.EXTRA_STATE; -import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE; import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; -import static android.bluetooth.BluetoothAdapter.STATE_ON; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +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.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION; @@ -51,10 +61,33 @@ import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMI @ParametersNotNullByDefault public abstract class KeyAgreementActivity extends BriarActivity implements BaseFragmentListener, IntroScreenSeenListener, - KeyAgreementEventListener { + KeyAgreementEventListener, EventListener { - private enum BluetoothState { - UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED, DISCOVERABLE + private enum BluetoothDecision { + /** + * We haven't asked the user about Bluetooth discoverability. + */ + UNKNOWN, + + /** + * The device doesn't have a Bluetooth adapter. + */ + NO_ADAPTER, + + /** + * We're waiting for the user to accept or refuse discoverability. + */ + WAITING, + + /** + * The user has accepted discoverability. + */ + ACCEPTED, + + /** + * The user has refused discoverability. + */ + REFUSED } private enum Permission { @@ -62,11 +95,14 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } private static final Logger LOG = - Logger.getLogger(KeyAgreementActivity.class.getName()); + getLogger(KeyAgreementActivity.class.getName()); @Inject EventBus eventBus; + @Inject + PluginManager pluginManager; + /** * Set to true in onPostResume() and false in onPause(). This prevents the * QR code fragment from being shown if onRequestPermissionsResult() is @@ -74,21 +110,36 @@ public abstract class KeyAgreementActivity extends BriarActivity implements * https://issuetracker.google.com/issues/37067655. */ private boolean isResumed = false; + /** * Set to true when the continue button is clicked, and false when the QR * code fragment is shown. This prevents the QR code fragment from being * shown automatically before the continue button has been clicked. */ private boolean continueClicked = false; + /** * Records whether the Bluetooth adapter was already enabled before we * asked for Bluetooth discoverability, so we know whether to broadcast a * {@link BluetoothEnabledEvent}. */ private boolean wasAdapterEnabled = false; + + /** + * Records whether we've enabled the wifi plugin so we don't enable it more + * than once. + */ + private boolean hasEnabledWifi = false; + + /** + * Records whether we've enabled the Bluetooth plugin so we don't enable it + * more than once. + */ + private boolean hasEnabledBluetooth = false; + private Permission cameraPermission = Permission.UNKNOWN; private Permission locationPermission = Permission.UNKNOWN; - private BluetoothState bluetoothState = BluetoothState.UNKNOWN; + private BluetoothDecision bluetoothDecision = BluetoothDecision.UNKNOWN; private BroadcastReceiver bluetoothReceiver = null; @Override @@ -96,20 +147,17 @@ public abstract class KeyAgreementActivity extends BriarActivity implements component.inject(this); } - @SuppressWarnings("ConstantConditions") @Override public void onCreate(@Nullable Bundle state) { super.onCreate(state); setContentView(R.layout.activity_fragment_container_toolbar); Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); + requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true); if (state == null) { showInitialFragment(IntroFragment.newInstance()); } - IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_STATE_CHANGED); - filter.addAction(ACTION_SCAN_MODE_CHANGED); + IntentFilter filter = new IntentFilter(ACTION_SCAN_MODE_CHANGED); bluetoothReceiver = new BluetoothStateReceiver(); registerReceiver(bluetoothReceiver, filter); } @@ -122,18 +170,17 @@ public abstract class KeyAgreementActivity extends BriarActivity implements @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; } + return super.onOptionsItemSelected(item); } @Override public void onStart() { super.onStart(); + eventBus.addListener(this); // Permissions may have been granted manually while we were stopped cameraPermission = Permission.UNKNOWN; locationPermission = Permission.UNKNOWN; @@ -150,11 +197,22 @@ public abstract class KeyAgreementActivity extends BriarActivity implements private void showQrCodeFragmentIfAllowed() { if (isResumed && continueClicked && areEssentialPermissionsGranted()) { - if (bluetoothState == BluetoothState.UNKNOWN || - bluetoothState == BluetoothState.ENABLED) { - requestBluetoothDiscoverable(); - } else if (bluetoothState != BluetoothState.WAITING) { + if (isWifiReady() && isBluetoothReady()) { + LOG.info("Wifi and Bluetooth are ready"); showQrCodeFragment(); + } else { + if (shouldEnableWifi()) { + LOG.info("Enabling wifi plugin"); + hasEnabledWifi = true; + pluginManager.setPluginEnabled(LanTcpConstants.ID, true); + } + if (bluetoothDecision == BluetoothDecision.UNKNOWN) { + requestBluetoothDiscoverable(); + } else if (shouldEnableBluetooth()) { + LOG.info("Enabling Bluetooth plugin"); + hasEnabledBluetooth = true; + pluginManager.setPluginEnabled(BluetoothConstants.ID, true); + } } } } @@ -167,57 +225,108 @@ public abstract class KeyAgreementActivity extends BriarActivity implements locationPermission == Permission.PERMANENTLY_DENIED); } + private boolean isWifiReady() { + Plugin p = pluginManager.getPlugin(LanTcpConstants.ID); + if (p == null) return true; // Continue without wifi + State state = p.getState(); + // Wait for plugin to become enabled + return state == ACTIVE || state == INACTIVE; + } + + private boolean isBluetoothReady() { + if (bluetoothDecision == BluetoothDecision.UNKNOWN || + bluetoothDecision == BluetoothDecision.WAITING) { + // Wait for decision + return false; + } + if (bluetoothDecision == BluetoothDecision.NO_ADAPTER + || bluetoothDecision == BluetoothDecision.REFUSED) { + // Continue without Bluetooth + return true; + } + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt == null) return true; // Continue without Bluetooth + if (bt.getScanMode() != SCAN_MODE_CONNECTABLE_DISCOVERABLE) { + // Wait for adapter to become discoverable + return false; + } + Plugin p = pluginManager.getPlugin(BluetoothConstants.ID); + if (p == null) return true; // Continue without Bluetooth + // Wait for plugin to become active + return p.getState() == ACTIVE; + } + + private boolean shouldEnableWifi() { + if (hasEnabledWifi) return false; + Plugin p = pluginManager.getPlugin(LanTcpConstants.ID); + if (p == null) return false; + State state = p.getState(); + return state == STARTING_STOPPING || state == DISABLED; + } + + private void requestBluetoothDiscoverable() { + BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); + if (bt == null) { + bluetoothDecision = BluetoothDecision.NO_ADAPTER; + showQrCodeFragmentIfAllowed(); + } else { + Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); + if (i.resolveActivity(getPackageManager()) != null) { + LOG.info("Asking for Bluetooth discoverability"); + bluetoothDecision = BluetoothDecision.WAITING; + wasAdapterEnabled = bt.isEnabled(); + startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE); + } else { + bluetoothDecision = BluetoothDecision.NO_ADAPTER; + showQrCodeFragmentIfAllowed(); + } + } + } + + private boolean shouldEnableBluetooth() { + if (bluetoothDecision != BluetoothDecision.ACCEPTED) return false; + if (hasEnabledBluetooth) return false; + Plugin p = pluginManager.getPlugin(BluetoothConstants.ID); + if (p == null) return false; + State state = p.getState(); + return state == STARTING_STOPPING || state == DISABLED; + } + @Override protected void onPause() { super.onPause(); isResumed = false; } + @Override + protected void onStop() { + super.onStop(); + eventBus.removeListener(this); + } + @Override public void showNextScreen() { continueClicked = true; if (checkPermissions()) showQrCodeFragmentIfAllowed(); } - private void requestBluetoothDiscoverable() { - BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - if (bt == null) { - setBluetoothState(BluetoothState.NO_ADAPTER); - } else { - Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE); - if (i.resolveActivity(getPackageManager()) != null) { - setBluetoothState(BluetoothState.WAITING); - wasAdapterEnabled = bt.isEnabled(); - startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE); - } else { - setBluetoothState(BluetoothState.NO_ADAPTER); - } - } - } - - private void setBluetoothState(BluetoothState bluetoothState) { - LOG.info("Setting Bluetooth state to " + bluetoothState); - this.bluetoothState = bluetoothState; - if (!wasAdapterEnabled && bluetoothState == BluetoothState.ENABLED) { - eventBus.broadcast(new BluetoothEnabledEvent()); - wasAdapterEnabled = true; - } - showQrCodeFragmentIfAllowed(); - } - @Override - public void onActivityResult(int request, int result, Intent data) { + public void onActivityResult(int request, int result, + @Nullable Intent data) { if (request == REQUEST_BLUETOOTH_DISCOVERABLE) { if (result == RESULT_CANCELED) { - setBluetoothState(BluetoothState.REFUSED); + LOG.info("Bluetooth discoverability was refused"); + bluetoothDecision = BluetoothDecision.REFUSED; } else { - // If Bluetooth is already discoverable, show the QR code - - // otherwise wait for the state or scan mode to change - BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter(); - if (bt == null) throw new AssertionError(); - if (bt.getScanMode() == SCAN_MODE_CONNECTABLE_DISCOVERABLE) - setBluetoothState(BluetoothState.DISCOVERABLE); + LOG.info("Bluetooth discoverability was accepted"); + bluetoothDecision = BluetoothDecision.ACCEPTED; + if (!wasAdapterEnabled) { + LOG.info("Bluetooth adapter was enabled by us"); + eventBus.broadcast(new BluetoothEnabledEvent()); + wasAdapterEnabled = true; + } } + showQrCodeFragmentIfAllowed(); } else super.onActivityResult(request, result, data); } @@ -227,7 +336,12 @@ public abstract class KeyAgreementActivity extends BriarActivity implements continueClicked = false; // If we return to the intro fragment, ask for Bluetooth // discoverability again before showing the QR code fragment - bluetoothState = BluetoothState.UNKNOWN; + bluetoothDecision = BluetoothDecision.UNKNOWN; + // If we return to the intro fragment, we may need to enable wifi and + // Bluetooth again + hasEnabledWifi = false; + hasEnabledBluetooth = false; + // FIXME #824 FragmentManager fm = getSupportFragmentManager(); if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) { @@ -239,12 +353,6 @@ public abstract class KeyAgreementActivity extends BriarActivity implements } } - protected void showErrorFragment(@StringRes int errorResId) { - String errorMsg = getString(errorResId); - BaseFragment f = ContactExchangeErrorFragment.newInstance(errorMsg); - showNextFragment(f); - } - private boolean checkPermissions() { if (areEssentialPermissionsGranted()) return true; // If the camera permission has been permanently denied, ask the @@ -335,24 +443,30 @@ public abstract class KeyAgreementActivity extends BriarActivity implements permission); } + @Override + public void eventOccurred(Event e) { + if (e instanceof TransportStateEvent) { + TransportStateEvent t = (TransportStateEvent) e; + if (t.getTransportId().equals(BluetoothConstants.ID)) { + if (LOG.isLoggable(INFO)) { + LOG.info("Bluetooth state changed to " + t.getState()); + } + showQrCodeFragmentIfAllowed(); + } else if (t.getTransportId().equals(LanTcpConstants.ID)) { + if (LOG.isLoggable(INFO)) { + LOG.info("Wifi state changed to " + t.getState()); + } + showQrCodeFragmentIfAllowed(); + } + } + } + private class BluetoothStateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - if (ACTION_STATE_CHANGED.equals(action)) { - int state = intent.getIntExtra(EXTRA_STATE, 0); - if (state == STATE_ON) - setBluetoothState(BluetoothState.ENABLED); - else setBluetoothState(BluetoothState.UNKNOWN); - } else if (ACTION_SCAN_MODE_CHANGED.equals(action)) { - int scanMode = intent.getIntExtra(EXTRA_SCAN_MODE, 0); - if (scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE) - setBluetoothState(BluetoothState.DISCOVERABLE); - else if (scanMode == SCAN_MODE_CONNECTABLE) - setBluetoothState(BluetoothState.ENABLED); - else setBluetoothState(BluetoothState.UNKNOWN); - } + LOG.info("Bluetooth scan mode changed"); + showQrCodeFragmentIfAllowed(); } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java index 1c71573c8..3cd9f53a1 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerActivity.java @@ -1,6 +1,5 @@ package org.briarproject.briar.android.navdrawer; -import android.annotation.SuppressLint; import android.content.Intent; import android.content.res.Configuration; import android.net.Uri; @@ -23,6 +22,7 @@ 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.LanTcpConstants; +import org.briarproject.bramble.api.plugin.Plugin.State; import org.briarproject.bramble.api.plugin.TorConstants; import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.briar.R; @@ -30,7 +30,6 @@ import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.BriarActivity; import org.briarproject.briar.android.blog.FeedFragment; import org.briarproject.briar.android.contact.ContactListFragment; -import org.briarproject.briar.android.controller.handler.UiResultHandler; import org.briarproject.briar.android.forum.ForumListFragment; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.BaseFragment.BaseFragmentListener; @@ -44,9 +43,11 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.UiThread; +import androidx.annotation.StringRes; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.widget.Toolbar; @@ -55,6 +56,8 @@ import androidx.core.content.ContextCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelProviders; import static android.view.View.GONE; import static android.view.View.VISIBLE; @@ -64,6 +67,9 @@ import static androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE; import static java.util.Objects.requireNonNull; import static java.util.logging.Logger.getLogger; import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState.RUNNING; +import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE; +import static org.briarproject.bramble.api.plugin.Plugin.State.ENABLING; +import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; import static org.briarproject.briar.android.BriarService.EXTRA_STARTUP_FAILED; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PASSWORD; import static org.briarproject.briar.android.navdrawer.IntentRouter.handleExternalIntent; @@ -72,8 +78,7 @@ import static org.briarproject.briar.android.util.UiUtils.getDaysUntilExpiry; @MethodsNotNullByDefault @ParametersNotNullByDefault public class NavDrawerActivity extends BriarActivity implements - BaseFragmentListener, TransportStateListener, - OnNavigationItemSelectedListener { + BaseFragmentListener, OnNavigationItemSelectedListener { private static final Logger LOG = getLogger(NavDrawerActivity.class.getName()); @@ -91,10 +96,13 @@ public class NavDrawerActivity extends BriarActivity implements public static Uri SIGN_OUT_URI = Uri.parse("briar-content://org.briarproject.briar/sign-out"); + private NavDrawerViewModel navDrawerViewModel; + private PluginViewModel pluginViewModel; private ActionBarDrawerToggle drawerToggle; @Inject - NavDrawerController controller; + ViewModelProvider.Factory viewModelFactory; + @Inject LifecycleManager lifecycleManager; @@ -115,6 +123,17 @@ public class NavDrawerActivity extends BriarActivity implements exitIfStartupFailed(getIntent()); setContentView(R.layout.activity_nav_drawer); + ViewModelProvider provider = + ViewModelProviders.of(this, viewModelFactory); + navDrawerViewModel = provider.get(NavDrawerViewModel.class); + pluginViewModel = provider.get(PluginViewModel.class); + + navDrawerViewModel.showExpiryWarning() + .observe(this, this::showExpiryWarning); + navDrawerViewModel.shouldAskForDozeWhitelisting().observe(this, ask -> { + if (ask) showDozeDialog(getString(R.string.setup_doze_intro)); + }); + Toolbar toolbar = findViewById(R.id.toolbar); drawerLayout = findViewById(R.id.drawer_layout); navigation = findViewById(R.id.navigation); @@ -131,7 +150,7 @@ public class NavDrawerActivity extends BriarActivity implements drawerLayout.addDrawerListener(drawerToggle); navigation.setNavigationItemSelectedListener(this); - initializeTransports(getLayoutInflater()); + initializeTransports(); transportsView.setAdapter(transportsAdapter); lockManager.isLockable().observe(this, this::setLockVisible); @@ -149,17 +168,10 @@ public class NavDrawerActivity extends BriarActivity implements } @Override - @SuppressLint("NewApi") public void onStart() { super.onStart(); - updateTransports(); lockManager.checkIfLockable(); - controller.showExpiryWarning(new UiResultHandler(this) { - @Override - public void onResultUi(Boolean expiry) { - if (expiry) showExpiryWarning(); - } - }); + navDrawerViewModel.checkExpiryWarning(); } @Override @@ -167,16 +179,7 @@ public class NavDrawerActivity extends BriarActivity implements @Nullable Intent data) { super.onActivityResult(request, result, data); if (request == REQUEST_PASSWORD && result == RESULT_OK) { - controller.shouldAskForDozeWhitelisting(this, - new UiResultHandler(this) { - @Override - public void onResultUi(Boolean ask) { - if (ask) { - showDozeDialog( - getString(R.string.setup_doze_intro)); - } - } - }); + navDrawerViewModel.checkDozeWhitelisting(); } } @@ -346,55 +349,37 @@ public class NavDrawerActivity extends BriarActivity implements if (item != null) item.setVisible(visible); } - private void showExpiryWarning() { + private void showExpiryWarning(boolean show) { int daysUntilExpiry = getDaysUntilExpiry(); - if (daysUntilExpiry < 0) signOut(); + if (daysUntilExpiry < 0) { + signOut(); + return; + } - // show expiry warning text ViewGroup expiryWarning = findViewById(R.id.expiryWarning); - TextView expiryWarningText = - expiryWarning.findViewById(R.id.expiryWarningText); - // make close button functional - ImageView expiryWarningClose = - expiryWarning.findViewById(R.id.expiryWarningClose); - - expiryWarningText.setText(getResources() - .getQuantityString(R.plurals.expiry_warning, - daysUntilExpiry, daysUntilExpiry)); - - expiryWarningClose.setOnClickListener(v -> { - controller.expiryWarningDismissed(); + if (show) { + // show expiry warning text + TextView expiryWarningText = + expiryWarning.findViewById(R.id.expiryWarningText); + String text = getResources().getQuantityString( + R.plurals.expiry_warning, daysUntilExpiry, daysUntilExpiry); + expiryWarningText.setText(text); + // make close button functional + ImageView expiryWarningClose = + expiryWarning.findViewById(R.id.expiryWarningClose); + expiryWarningClose.setOnClickListener(v -> + navDrawerViewModel.expiryWarningDismissed()); + expiryWarning.setVisibility(VISIBLE); + } else { expiryWarning.setVisibility(GONE); - }); - - expiryWarning.setVisibility(VISIBLE); + } } - private void initializeTransports(LayoutInflater inflater) { + private void initializeTransports() { transports = new ArrayList<>(3); - Transport tor = new Transport(); - tor.id = TorConstants.ID; - tor.enabled = controller.isTransportRunning(tor.id); - tor.iconId = R.drawable.transport_tor; - tor.textId = R.string.transport_tor; - transports.add(tor); - - Transport bt = new Transport(); - bt.id = BluetoothConstants.ID; - bt.enabled = controller.isTransportRunning(bt.id); - bt.iconId = R.drawable.transport_bt; - bt.textId = R.string.transport_bt; - transports.add(bt); - - Transport lan = new Transport(); - lan.id = LanTcpConstants.ID; - lan.enabled = controller.isTransportRunning(lan.id); - lan.iconId = R.drawable.transport_lan; - lan.textId = R.string.transport_lan; - transports.add(lan); - transportsAdapter = new BaseAdapter() { + @Override public int getCount() { return transports.size(); @@ -417,63 +402,67 @@ public class NavDrawerActivity extends BriarActivity implements if (convertView != null) { view = convertView; } else { + LayoutInflater inflater = getLayoutInflater(); view = inflater.inflate(R.layout.list_item_transport, parent, false); } Transport t = getItem(position); - int c; - if (t.enabled) { - c = ContextCompat.getColor(NavDrawerActivity.this, - R.color.briar_green_light); - } else { - c = ContextCompat.getColor(NavDrawerActivity.this, - android.R.color.tertiary_text_light); - } ImageView icon = view.findViewById(R.id.imageView); - icon.setImageDrawable(ContextCompat - .getDrawable(NavDrawerActivity.this, t.iconId)); - icon.setColorFilter(c); + icon.setImageDrawable(ContextCompat.getDrawable( + NavDrawerActivity.this, t.iconDrawable)); + icon.setColorFilter(ContextCompat.getColor( + NavDrawerActivity.this, t.iconColor)); TextView text = view.findViewById(R.id.textView); - text.setText(getString(t.textId)); + text.setText(getString(t.label)); return view; } }; + + transports.add(createTransport(TorConstants.ID, + R.drawable.transport_tor, R.string.transport_tor)); + transports.add(createTransport(LanTcpConstants.ID, + R.drawable.transport_lan, R.string.transport_lan)); + transports.add(createTransport(BluetoothConstants.ID, + R.drawable.transport_bt, R.string.transport_bt)); } - @UiThread - private void setTransport(TransportId id, boolean enabled) { - if (transports == null || transportsAdapter == null) return; - for (Transport t : transports) { - if (t.id.equals(id)) { - t.enabled = enabled; - transportsAdapter.notifyDataSetChanged(); - break; - } - } + @ColorRes + private int getIconColor(State state) { + if (state == ACTIVE) return R.color.briar_green_light; + else if (state == ENABLING) return R.color.briar_yellow; + else return android.R.color.tertiary_text_light; } - private void updateTransports() { - if (transports == null || transportsAdapter == null) return; - for (Transport t : transports) { - t.enabled = controller.isTransportRunning(t.id); - } - transportsAdapter.notifyDataSetChanged(); - } - - @Override - public void stateUpdate(TransportId id, boolean enabled) { - setTransport(id, enabled); + private Transport createTransport(TransportId id, + @DrawableRes int iconDrawable, @StringRes int label) { + int iconColor = getIconColor(STARTING_STOPPING); + Transport transport = new Transport(iconDrawable, label, iconColor); + pluginViewModel.getPluginState(id).observe(this, state -> { + transport.iconColor = getIconColor(state); + transportsAdapter.notifyDataSetChanged(); + }); + return transport; } private static class Transport { - private TransportId id; - private boolean enabled; - private int iconId; - private int textId; + @DrawableRes + private final int iconDrawable; + @StringRes + private final int label; + + @ColorRes + private int iconColor; + + private Transport(@DrawableRes int iconDrawable, @StringRes int label, + @ColorRes int iconColor) { + this.iconDrawable = iconDrawable; + this.label = label; + this.iconColor = iconColor; + } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerController.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerController.java deleted file mode 100644 index 44c8c8dfd..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerController.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.briarproject.briar.android.navdrawer; - -import android.content.Context; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.briar.android.controller.ActivityLifecycleController; -import org.briarproject.briar.android.controller.handler.ResultHandler; - -@NotNullByDefault -public interface NavDrawerController extends ActivityLifecycleController { - - boolean isTransportRunning(TransportId transportId); - - void showExpiryWarning(ResultHandler handler); - - void expiryWarningDismissed(); - - void shouldAskForDozeWhitelisting(Context ctx, - ResultHandler handler); - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerControllerImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerControllerImpl.java deleted file mode 100644 index 4af7eded3..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerControllerImpl.java +++ /dev/null @@ -1,182 +0,0 @@ -package org.briarproject.briar.android.navdrawer; - -import android.app.Activity; -import android.content.Context; - -import org.briarproject.bramble.api.db.DatabaseExecutor; -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.LifecycleManager; -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; -import org.briarproject.bramble.api.plugin.Plugin; -import org.briarproject.bramble.api.plugin.PluginManager; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.plugin.event.TransportDisabledEvent; -import org.briarproject.bramble.api.plugin.event.TransportEnabledEvent; -import org.briarproject.bramble.api.settings.Settings; -import org.briarproject.bramble.api.settings.SettingsManager; -import org.briarproject.briar.android.controller.DbControllerImpl; -import org.briarproject.briar.android.controller.handler.ResultHandler; - -import java.util.concurrent.Executor; -import java.util.logging.Logger; - -import javax.inject.Inject; - -import static java.util.concurrent.TimeUnit.DAYS; -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.util.LogUtils.logException; -import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; -import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; -import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE_ASK_AGAIN; -import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; -import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting; - -@MethodsNotNullByDefault -@ParametersNotNullByDefault -public class NavDrawerControllerImpl extends DbControllerImpl - implements NavDrawerController, EventListener { - - private static final Logger LOG = - getLogger(NavDrawerControllerImpl.class.getName()); - - private static final String EXPIRY_DATE_WARNING = "expiryDateWarning"; - - private final PluginManager pluginManager; - private final SettingsManager settingsManager; - private final EventBus eventBus; - - // UI thread - private TransportStateListener listener; - - @Inject - NavDrawerControllerImpl(@DatabaseExecutor Executor dbExecutor, - LifecycleManager lifecycleManager, PluginManager pluginManager, - SettingsManager settingsManager, EventBus eventBus) { - super(dbExecutor, lifecycleManager); - this.pluginManager = pluginManager; - this.settingsManager = settingsManager; - this.eventBus = eventBus; - } - - @Override - public void onActivityCreate(Activity activity) { - listener = (TransportStateListener) activity; - } - - @Override - public void onActivityStart() { - eventBus.addListener(this); - } - - @Override - public void onActivityStop() { - eventBus.removeListener(this); - } - - @Override - public void onActivityDestroy() { - } - - @Override - public void eventOccurred(Event e) { - if (e instanceof TransportEnabledEvent) { - TransportId id = ((TransportEnabledEvent) e).getTransportId(); - if (LOG.isLoggable(INFO)) { - LOG.info("TransportEnabledEvent: " + id.getString()); - } - listener.stateUpdate(id, true); - } else if (e instanceof TransportDisabledEvent) { - TransportId id = ((TransportDisabledEvent) e).getTransportId(); - if (LOG.isLoggable(INFO)) { - LOG.info("TransportDisabledEvent: " + id.getString()); - } - listener.stateUpdate(id, false); - } - } - - @Override - public void showExpiryWarning(ResultHandler handler) { - if (!IS_DEBUG_BUILD) { - handler.onResult(false); - return; - } - runOnDbThread(() -> { - try { - Settings settings = - settingsManager.getSettings(SETTINGS_NAMESPACE); - int warningInt = settings.getInt(EXPIRY_DATE_WARNING, 0); - - if (warningInt == 0) { - // we have not warned before - handler.onResult(true); - } else { - long warningLong = warningInt * 1000L; - long now = System.currentTimeMillis(); - long daysSinceLastWarning = - (now - warningLong) / DAYS.toMillis(1); - long daysBeforeExpiry = - (EXPIRY_DATE - now) / DAYS.toMillis(1); - - if (daysSinceLastWarning >= 30) { - handler.onResult(true); - } else if (daysBeforeExpiry <= 3 && - daysSinceLastWarning > 0) { - handler.onResult(true); - } else { - handler.onResult(false); - } - } - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - @Override - public void expiryWarningDismissed() { - runOnDbThread(() -> { - try { - Settings settings = new Settings(); - int date = (int) (System.currentTimeMillis() / 1000L); - settings.putInt(EXPIRY_DATE_WARNING, date); - settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); - } catch (DbException e) { - logException(LOG, WARNING, e); - } - }); - } - - @Override - public void shouldAskForDozeWhitelisting(Context ctx, - ResultHandler handler) { - // check this first, to hit the DbThread only when really necessary - if (!needsDozeWhitelisting(ctx)) { - handler.onResult(false); - return; - } - runOnDbThread(() -> { - try { - Settings settings = - settingsManager.getSettings(SETTINGS_NAMESPACE); - boolean ask = settings.getBoolean(DOZE_ASK_AGAIN, true); - handler.onResult(ask); - } catch (DbException e) { - logException(LOG, WARNING, e); - handler.onResult(true); - } - }); - } - - @Override - public boolean isTransportRunning(TransportId transportId) { - Plugin plugin = pluginManager.getPlugin(transportId); - return plugin != null && plugin.isRunning(); - } - -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerModule.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerModule.java new file mode 100644 index 000000000..e7282b1c7 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerModule.java @@ -0,0 +1,23 @@ +package org.briarproject.briar.android.navdrawer; + +import org.briarproject.briar.android.viewmodel.ViewModelKey; + +import androidx.lifecycle.ViewModel; +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.IntoMap; + +@Module +public abstract class NavDrawerModule { + + @Binds + @IntoMap + @ViewModelKey(NavDrawerViewModel.class) + abstract ViewModel bindNavDrawerViewModel( + NavDrawerViewModel navDrawerViewModel); + + @Binds + @IntoMap + @ViewModelKey(PluginViewModel.class) + abstract ViewModel bindPluginViewModel(PluginViewModel pluginViewModel); +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java new file mode 100644 index 000000000..37f26b6e4 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/NavDrawerViewModel.java @@ -0,0 +1,136 @@ +package org.briarproject.briar.android.navdrawer; + +import android.app.Application; + +import org.briarproject.bramble.api.db.DatabaseExecutor; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.settings.Settings; +import org.briarproject.bramble.api.settings.SettingsManager; + +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.UiThread; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; +import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; +import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; +import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE_ASK_AGAIN; +import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE; +import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting; + +@NotNullByDefault +public class NavDrawerViewModel extends AndroidViewModel { + + private static final Logger LOG = + getLogger(NavDrawerViewModel.class.getName()); + + private static final String EXPIRY_DATE_WARNING = "expiryDateWarning"; + + @DatabaseExecutor + private final Executor dbExecutor; + private final SettingsManager settingsManager; + + private final MutableLiveData showExpiryWarning = + new MutableLiveData<>(); + private final MutableLiveData shouldAskForDozeWhitelisting = + new MutableLiveData<>(); + + @Inject + NavDrawerViewModel(Application app, @DatabaseExecutor Executor dbExecutor, + SettingsManager settingsManager) { + super(app); + this.dbExecutor = dbExecutor; + this.settingsManager = settingsManager; + } + + LiveData showExpiryWarning() { + return showExpiryWarning; + } + + @UiThread + void checkExpiryWarning() { + if (!IS_DEBUG_BUILD) { + showExpiryWarning.setValue(false); + return; + } + dbExecutor.execute(() -> { + try { + Settings settings = + settingsManager.getSettings(SETTINGS_NAMESPACE); + int warningInt = settings.getInt(EXPIRY_DATE_WARNING, 0); + + if (warningInt == 0) { + // we have not warned before + showExpiryWarning.postValue(true); + } else { + long warningLong = warningInt * 1000L; + long now = System.currentTimeMillis(); + long daysSinceLastWarning = + (now - warningLong) / DAYS.toMillis(1); + long daysBeforeExpiry = + (EXPIRY_DATE - now) / DAYS.toMillis(1); + + if (daysSinceLastWarning >= 30) { + showExpiryWarning.postValue(true); + } else if (daysBeforeExpiry <= 3 && + daysSinceLastWarning > 0) { + showExpiryWarning.postValue(true); + } else { + showExpiryWarning.postValue(false); + } + } + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + @UiThread + void expiryWarningDismissed() { + showExpiryWarning.setValue(false); + dbExecutor.execute(() -> { + try { + Settings settings = new Settings(); + int date = (int) (System.currentTimeMillis() / 1000L); + settings.putInt(EXPIRY_DATE_WARNING, date); + settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); + } catch (DbException e) { + logException(LOG, WARNING, e); + } + }); + } + + LiveData shouldAskForDozeWhitelisting() { + return shouldAskForDozeWhitelisting; + } + + @UiThread + void checkDozeWhitelisting() { + // check this first, to hit the DbThread only when really necessary + if (!needsDozeWhitelisting(getApplication())) { + shouldAskForDozeWhitelisting.setValue(false); + return; + } + dbExecutor.execute(() -> { + try { + Settings settings = + settingsManager.getSettings(SETTINGS_NAMESPACE); + boolean ask = settings.getBoolean(DOZE_ASK_AGAIN, true); + shouldAskForDozeWhitelisting.postValue(ask); + } catch (DbException e) { + logException(LOG, WARNING, e); + shouldAskForDozeWhitelisting.postValue(true); + } + }); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java new file mode 100644 index 000000000..8badce8c4 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/PluginViewModel.java @@ -0,0 +1,92 @@ +package org.briarproject.briar.android.navdrawer; + +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.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.BluetoothConstants; +import org.briarproject.bramble.api.plugin.LanTcpConstants; +import org.briarproject.bramble.api.plugin.Plugin; +import org.briarproject.bramble.api.plugin.Plugin.State; +import org.briarproject.bramble.api.plugin.PluginManager; +import org.briarproject.bramble.api.plugin.TorConstants; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.event.TransportStateEvent; + +import java.util.logging.Logger; + +import javax.inject.Inject; + +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING; + +@NotNullByDefault +public class PluginViewModel extends ViewModel implements EventListener { + + private static final Logger LOG = + getLogger(PluginViewModel.class.getName()); + + private final PluginManager pluginManager; + private final EventBus eventBus; + + private final MutableLiveData torPluginState = + new MutableLiveData<>(); + private final MutableLiveData wifiPluginState = + new MutableLiveData<>(); + private final MutableLiveData btPluginState = + new MutableLiveData<>(); + + @Inject + PluginViewModel(PluginManager pluginManager, EventBus eventBus) { + this.pluginManager = pluginManager; + this.eventBus = eventBus; + eventBus.addListener(this); + torPluginState.setValue(getTransportState(TorConstants.ID)); + wifiPluginState.setValue(getTransportState(LanTcpConstants.ID)); + btPluginState.setValue(getTransportState(BluetoothConstants.ID)); + } + + @Override + protected void onCleared() { + eventBus.removeListener(this); + } + + @Override + public void eventOccurred(Event e) { + if (e instanceof TransportStateEvent) { + TransportStateEvent t = (TransportStateEvent) e; + TransportId id = t.getTransportId(); + State state = t.getState(); + if (LOG.isLoggable(INFO)) { + LOG.info("TransportStateEvent: " + id + " is " + state); + } + MutableLiveData liveData = getPluginLiveData(id); + if (liveData != null) liveData.postValue(state); + } + } + + LiveData getPluginState(TransportId id) { + LiveData liveData = getPluginLiveData(id); + if (liveData == null) throw new IllegalArgumentException(); + return liveData; + } + + private State getTransportState(TransportId id) { + Plugin plugin = pluginManager.getPlugin(id); + return plugin == null ? STARTING_STOPPING : plugin.getState(); + } + + @Nullable + private MutableLiveData getPluginLiveData(TransportId id) { + if (id.equals(TorConstants.ID)) return torPluginState; + else if (id.equals(LanTcpConstants.ID)) return wifiPluginState; + else if (id.equals(BluetoothConstants.ID)) return btPluginState; + else return null; + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportStateListener.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportStateListener.java deleted file mode 100644 index f47e311e3..000000000 --- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportStateListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.briarproject.briar.android.navdrawer; - -import org.briarproject.bramble.api.plugin.TransportId; - -import androidx.annotation.UiThread; - -interface TransportStateListener { - - @UiThread - void stateUpdate(TransportId id, boolean enabled); -} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java index 253c3a94f..8c5e3e6e3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportPrimer.java @@ -21,6 +21,8 @@ import org.briarproject.briar.android.logging.BriefLogFormatter; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; @@ -185,12 +187,18 @@ public class BriarReportPrimer implements ReportPrimer { WifiInfo wifiInfo = wm.getConnectionInfo(); if (wifiInfo != null) { int ip = wifiInfo.getIpAddress(); // Nice API, Google - int ip1 = ip & 0xFF; - int ip2 = (ip >> 8) & 0xFF; - int ip3 = (ip >> 16) & 0xFF; - int ip4 = (ip >> 24) & 0xFF; - String address = ip1 + "." + ip2 + "." + ip3 + "." + ip4; - customData.put("Wi-Fi address", scrubInetAddress(address)); + byte[] ipBytes = new byte[4]; + ipBytes[0] = (byte) (ip & 0xFF); + ipBytes[1] = (byte) ((ip >> 8) & 0xFF); + ipBytes[2] = (byte) ((ip >> 16) & 0xFF); + ipBytes[3] = (byte) ((ip >> 24) & 0xFF); + try { + InetAddress address = InetAddress.getByAddress(ipBytes); + customData.put("Wi-Fi address", + scrubInetAddress(address)); + } catch (UnknownHostException ignored) { + // Should only be thrown if address has illegal length + } } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java index dc7096db0..0c41e6fba 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/SettingsFragment.java @@ -21,6 +21,7 @@ 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.LanTcpConstants; import org.briarproject.bramble.api.plugin.TorConstants; import org.briarproject.bramble.api.settings.Settings; import org.briarproject.bramble.api.settings.SettingsManager; @@ -41,6 +42,7 @@ import java.util.logging.Logger; import javax.inject.Inject; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.content.ContextCompat; import androidx.core.text.TextUtilsCompat; @@ -72,10 +74,11 @@ import static android.widget.Toast.LENGTH_SHORT; import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_LTR; import static java.util.logging.Level.INFO; import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.api.plugin.BluetoothConstants.PREF_BT_ENABLE; +import static org.briarproject.bramble.api.plugin.Plugin.PREF_PLUGIN_ENABLE; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_MOBILE; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_AUTOMATIC; +import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_NETWORK_NEVER; import static org.briarproject.bramble.api.plugin.TorConstants.PREF_TOR_ONLY_WHEN_CHARGING; import static org.briarproject.bramble.util.LogUtils.logDuration; import static org.briarproject.bramble.util.LogUtils.logException; @@ -84,6 +87,7 @@ import static org.briarproject.briar.android.BriarApplication.ENTRY_ACTIVITY; import static org.briarproject.briar.android.TestingConstants.IS_DEBUG_BUILD; import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_RINGTONE; import static org.briarproject.briar.android.navdrawer.NavDrawerActivity.SIGN_OUT_URI; +import static org.briarproject.briar.android.util.UiUtils.getCountryDisplayName; import static org.briarproject.briar.android.util.UiUtils.hasScreenLock; import static org.briarproject.briar.android.util.UiUtils.triggerFeedback; import static org.briarproject.briar.api.android.AndroidNotificationManager.BLOG_CHANNEL_ID; @@ -105,16 +109,24 @@ public class SettingsFragment extends PreferenceFragmentCompat implements EventListener, OnPreferenceChangeListener { public static final String SETTINGS_NAMESPACE = "android-ui"; - public static final String BT_NAMESPACE = BluetoothConstants.ID.getString(); - public static final String TOR_NAMESPACE = TorConstants.ID.getString(); public static final String LANGUAGE = "pref_key_language"; public static final String PREF_SCREEN_LOCK = "pref_key_lock"; public static final String PREF_SCREEN_LOCK_TIMEOUT = "pref_key_lock_timeout"; public static final String NOTIFY_SIGN_IN = "pref_key_notify_sign_in"; - public static final String TOR_NETWORK = "pref_key_tor_network"; - public static final String TOR_MOBILE = "pref_key_tor_mobile_data"; - public static final String TOR_ONLY_WHEN_CHARGING = + + private static final String BT_NAMESPACE = + BluetoothConstants.ID.getString(); + private static final String BT_ENABLE = "pref_key_bluetooth"; + + private static final String WIFI_NAMESPACE = LanTcpConstants.ID.getString(); + private static final String WIFI_ENABLE = "pref_key_wifi"; + + private static final String TOR_NAMESPACE = TorConstants.ID.getString(); + private static final String TOR_ENABLE = "pref_key_tor_enable"; + private static final String TOR_NETWORK = "pref_key_tor_network"; + private static final String TOR_MOBILE = "pref_key_tor_mobile_data"; + private static final String TOR_ONLY_WHEN_CHARGING = "pref_key_tor_only_when_charging"; private static final Logger LOG = @@ -122,7 +134,9 @@ public class SettingsFragment extends PreferenceFragmentCompat private SettingsActivity listener; private ListPreference language; - private ListPreference enableBluetooth; + private SwitchPreference enableBluetooth; + private SwitchPreference enableWifi; + private SwitchPreference enableTor; private ListPreference torNetwork; private SwitchPreference torMobile; private SwitchPreference torOnlyWhenCharging; @@ -137,7 +151,7 @@ public class SettingsFragment extends PreferenceFragmentCompat private Preference notifySound; // Fields that are accessed from background threads must be volatile - private volatile Settings settings, btSettings, torSettings; + private volatile Settings settings, btSettings, wifiSettings, torSettings; private volatile boolean settingsLoaded = false; @Inject @@ -163,28 +177,23 @@ public class SettingsFragment extends PreferenceFragmentCompat public void onCreatePreferences(Bundle bundle, String s) { addPreferencesFromResource(R.xml.settings); - language = (ListPreference) findPreference(LANGUAGE); + language = findPreference(LANGUAGE); setLanguageEntries(); - ListPreference theme = - (ListPreference) findPreference("pref_key_theme"); - enableBluetooth = (ListPreference) findPreference("pref_key_bluetooth"); - torNetwork = (ListPreference) findPreference(TOR_NETWORK); - torMobile = (SwitchPreference) findPreference(TOR_MOBILE); - torOnlyWhenCharging = - (SwitchPreference) findPreference(TOR_ONLY_WHEN_CHARGING); - screenLock = (SwitchPreference) findPreference(PREF_SCREEN_LOCK); - screenLockTimeout = - (ListPreference) findPreference(PREF_SCREEN_LOCK_TIMEOUT); - notifyPrivateMessages = (SwitchPreference) findPreference( - "pref_key_notify_private_messages"); - notifyGroupMessages = (SwitchPreference) findPreference( - "pref_key_notify_group_messages"); - notifyForumPosts = (SwitchPreference) findPreference( - "pref_key_notify_forum_posts"); - notifyBlogPosts = (SwitchPreference) findPreference( - "pref_key_notify_blog_posts"); - notifyVibration = (SwitchPreference) findPreference( - "pref_key_notify_vibration"); + ListPreference theme = findPreference("pref_key_theme"); + enableBluetooth = findPreference(BT_ENABLE); + enableWifi = findPreference(WIFI_ENABLE); + enableTor = findPreference(TOR_ENABLE); + torNetwork = findPreference(TOR_NETWORK); + torMobile = findPreference(TOR_MOBILE); + torOnlyWhenCharging = findPreference(TOR_ONLY_WHEN_CHARGING); + screenLock = findPreference(PREF_SCREEN_LOCK); + screenLockTimeout = findPreference(PREF_SCREEN_LOCK_TIMEOUT); + notifyPrivateMessages = + findPreference("pref_key_notify_private_messages"); + notifyGroupMessages = findPreference("pref_key_notify_group_messages"); + notifyForumPosts = findPreference("pref_key_notify_forum_posts"); + notifyBlogPosts = findPreference("pref_key_notify_blog_posts"); + notifyVibration = findPreference("pref_key_notify_vibration"); notifySound = findPreference("pref_key_notify_sound"); language.setOnPreferenceChangeListener(this); @@ -194,8 +203,7 @@ public class SettingsFragment extends PreferenceFragmentCompat UiUtils.setTheme(getActivity(), (String) newValue); // bring up parent activity, so it can change its theme as well // upstream bug: https://issuetracker.google.com/issues/38352704 - Intent intent = - new Intent(getActivity(), ENTRY_ACTIVITY); + Intent intent = new Intent(getActivity(), ENTRY_ACTIVITY); intent.setFlags( FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK); startActivity(intent); @@ -207,6 +215,8 @@ public class SettingsFragment extends PreferenceFragmentCompat return true; }); enableBluetooth.setOnPreferenceChangeListener(this); + enableWifi.setOnPreferenceChangeListener(this); + enableTor.setOnPreferenceChangeListener(this); torNetwork.setOnPreferenceChangeListener(this); torMobile.setOnPreferenceChangeListener(this); torOnlyWhenCharging.setOnPreferenceChangeListener(this); @@ -249,8 +259,9 @@ public class SettingsFragment extends PreferenceFragmentCompat } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); ColorDrawable divider = new ColorDrawable( ContextCompat.getColor(requireContext(), R.color.divider)); @@ -325,17 +336,13 @@ public class SettingsFragment extends PreferenceFragmentCompat // Look up country name in the user's chosen language if available String country = locationUtils.getCurrentCountry(); - String countryName = country; - for (Locale locale : Locale.getAvailableLocales()) { - if (locale.getCountry().equalsIgnoreCase(country)) { - countryName = locale.getDisplayCountry(); - break; - } - } + String countryName = getCountryDisplayName(country); + boolean blocked = circumventionProvider.isTorProbablyBlocked(country); boolean useBridges = circumventionProvider.doBridgesWork(country); - String setting = getString(R.string.tor_network_setting_without_bridges); + String setting = + getString(R.string.tor_network_setting_without_bridges); if (blocked && useBridges) { setting = getString(R.string.tor_network_setting_with_bridges); } else if (blocked) { @@ -352,6 +359,7 @@ public class SettingsFragment extends PreferenceFragmentCompat long start = now(); settings = settingsManager.getSettings(SETTINGS_NAMESPACE); btSettings = settingsManager.getSettings(BT_NAMESPACE); + wifiSettings = settingsManager.getSettings(WIFI_NAMESPACE); torSettings = settingsManager.getSettings(TOR_NAMESPACE); settingsLoaded = true; logDuration(LOG, "Loading settings", start); @@ -362,14 +370,35 @@ public class SettingsFragment extends PreferenceFragmentCompat }); } + // TODO: Remove after a reasonable migration period (added 2020-06-25) + private Settings migrateTorSettings(Settings s) { + int network = s.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_AUTOMATIC); + if (network == PREF_TOR_NETWORK_NEVER) { + s.putInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_AUTOMATIC); + s.putBoolean(PREF_PLUGIN_ENABLE, false); + // We don't need to save the migrated settings - the Tor plugin is + // responsible for that. This code just handles the case where the + // settings are loaded before the plugin migrates them. + } + return s; + } + private void displaySettings() { listener.runOnUiThreadUnlessDestroyed(() -> { // due to events, we might try to display before a load completed if (!settingsLoaded) return; boolean btEnabledSetting = - btSettings.getBoolean(PREF_BT_ENABLE, false); - enableBluetooth.setValue(Boolean.toString(btEnabledSetting)); + btSettings.getBoolean(PREF_PLUGIN_ENABLE, false); + enableBluetooth.setChecked(btEnabledSetting); + + boolean wifiEnabledSetting = + wifiSettings.getBoolean(PREF_PLUGIN_ENABLE, false); + enableWifi.setChecked(wifiEnabledSetting); + + boolean torEnabledSetting = + torSettings.getBoolean(PREF_PLUGIN_ENABLE, true); + enableTor.setChecked(torEnabledSetting); int torNetworkSetting = torSettings.getInt(PREF_TOR_NETWORK, PREF_TOR_NETWORK_AUTOMATIC); @@ -443,6 +472,8 @@ public class SettingsFragment extends PreferenceFragmentCompat // - pref_key_lock (screenLock -> displayScreenLockSetting()) // - pref_key_lock_timeout (screenLockTimeout) enableBluetooth.setEnabled(enabled); + enableWifi.setEnabled(enabled); + enableTor.setEnabled(enabled); torNetwork.setEnabled(enabled); torMobile.setEnabled(enabled); torOnlyWhenCharging.setEnabled(enabled); @@ -545,8 +576,14 @@ public class SettingsFragment extends PreferenceFragmentCompat languageChanged((String) newValue); return false; } else if (preference == enableBluetooth) { - boolean btSetting = Boolean.valueOf((String) newValue); - storeBluetoothSettings(btSetting); + boolean btSetting = (Boolean) newValue; + storeBluetoothSetting(btSetting); + } else if (preference == enableWifi) { + boolean wifiSetting = (Boolean) newValue; + storeWifiSetting(wifiSetting); + } else if (preference == enableTor) { + boolean torEnabledSetting = (Boolean) newValue; + storeTorEnabledSetting(torEnabledSetting); } else if (preference == torNetwork) { int torNetworkSetting = Integer.valueOf((String) newValue); storeTorNetworkSetting(torNetworkSetting); @@ -610,6 +647,12 @@ public class SettingsFragment extends PreferenceFragmentCompat builder.show(); } + private void storeTorEnabledSetting(boolean torEnabledSetting) { + Settings s = new Settings(); + s.putBoolean(PREF_PLUGIN_ENABLE, torEnabledSetting); + mergeSettings(s, TOR_NAMESPACE); + } + private void storeTorNetworkSetting(int torNetworkSetting) { Settings s = new Settings(); s.putInt(PREF_TOR_NETWORK, torNetworkSetting); @@ -628,12 +671,18 @@ public class SettingsFragment extends PreferenceFragmentCompat mergeSettings(s, TOR_NAMESPACE); } - private void storeBluetoothSettings(boolean btSetting) { + private void storeBluetoothSetting(boolean btSetting) { Settings s = new Settings(); - s.putBoolean(PREF_BT_ENABLE, btSetting); + s.putBoolean(PREF_PLUGIN_ENABLE, btSetting); mergeSettings(s, BT_NAMESPACE); } + private void storeWifiSetting(boolean wifiSetting) { + Settings s = new Settings(); + s.putBoolean(PREF_PLUGIN_ENABLE, wifiSetting); + mergeSettings(s, WIFI_NAMESPACE); + } + private void storeSettings(Settings s) { mergeSettings(s, SETTINGS_NAMESPACE); } @@ -696,9 +745,13 @@ public class SettingsFragment extends PreferenceFragmentCompat LOG.info("Bluetooth settings updated"); btSettings = s.getSettings(); displaySettings(); + } else if (namespace.equals(WIFI_NAMESPACE)) { + LOG.info("Wifi settings updated"); + wifiSettings = s.getSettings(); + displaySettings(); } else if (namespace.equals(TOR_NAMESPACE)) { LOG.info("Tor settings updated"); - torSettings = s.getSettings(); + torSettings = migrateTorSettings(s.getSettings()); displaySettings(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index f4bdb966a..bf3805232 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -6,6 +6,7 @@ import android.app.KeyguardManager; import android.content.Context; import android.content.DialogInterface.OnClickListener; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.PowerManager; import android.text.Html; @@ -38,9 +39,12 @@ import org.briarproject.briar.R; import org.briarproject.briar.android.view.ArticleMovementMethod; import org.briarproject.briar.android.widget.LinkDialogFragment; +import java.util.Locale; + import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.UiThread; @@ -79,7 +83,10 @@ import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO; import static androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES; import static androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode; +import static androidx.core.content.ContextCompat.getColor; +import static androidx.core.content.ContextCompat.getDrawable; import static androidx.core.content.ContextCompat.getSystemService; +import static androidx.core.graphics.drawable.DrawableCompat.setTint; import static androidx.core.view.ViewCompat.LAYOUT_DIRECTION_RTL; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.DAYS; @@ -401,4 +408,20 @@ public class UiUtils { return ctx.getResources().getConfiguration().getLayoutDirection() == LAYOUT_DIRECTION_RTL; } + + public static String getCountryDisplayName(String isoCode) { + for (Locale locale : Locale.getAvailableLocales()) { + if (locale.getCountry().equalsIgnoreCase(isoCode)) { + return locale.getDisplayCountry(); + } + } + // Name is unknown + return isoCode; + } + + public static Drawable getDialogIcon(Context ctx, @DrawableRes int resId) { + Drawable icon = getDrawable(ctx, resId); + setTint(requireNonNull(icon), getColor(ctx, R.color.color_primary)); + return icon; + } } diff --git a/briar-android/src/main/res/menu/navigation_drawer.xml b/briar-android/src/main/res/menu/navigation_drawer.xml index 6f3606d0f..c24131755 100644 --- a/briar-android/src/main/res/menu/navigation_drawer.xml +++ b/briar-android/src/main/res/menu/navigation_drawer.xml @@ -1,6 +1,6 @@ -

+ + android:visible="false" + tools:visible="false" /> - - true - false - - - @string/bluetooth_setting_enabled - @string/bluetooth_setting_disabled - - @string/tor_network_setting_automatic @string/tor_network_setting_without_bridges @string/tor_network_setting_with_bridges - @string/tor_network_setting_never 0 1 2 - 3 @@ -66,6 +55,7 @@ zh-CN zh-TW + @string/pref_theme_light @string/pref_theme_dark diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 341e2c4ab..482b5ce1a 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -108,7 +108,6 @@ Delete Accept Decline - Options Online Offline Send @@ -190,7 +189,6 @@ Add Contact at a Distance Add contact nearby Add contact at a distance - Give contact a nickname Enter the link from your contact here Contact\'s link Paste @@ -208,7 +206,6 @@ There are pending contact requests Pending Contact Requests No pending contacts - Connecting… Waiting for contact to come online… Connecting… Adding contact… @@ -229,9 +226,6 @@ New contact added. %d new contacts added. - Adding this contact is taking longer than usual - Cannot Connect to Contact - Adding this contact is taking longer than usual.\n\nPlease check that your contact has received your link and added you: No Internet connection Duplicate Link You already have a pending contact with this link: %s @@ -261,7 +255,6 @@ Make Introduction Your introduction has been sent. There was an error making the introduction. - Error when responding to introduction You have asked to introduce %1$s to %2$s. %1$s has asked to introduce you to %2$s. Do you want to add %2$s to your contact list? %1$s has asked to introduce you to %2$s, but %2$s is already in your contact list. Since %1$s might not know that, you can still respond: @@ -443,20 +436,21 @@ Automatic (Daytime) System Default - - Networks - Connect via Bluetooth - Whenever contacts are nearby - Only when adding contacts - Connect via Internet (Tor) + + Connections + Connect to contacts via Bluetooth + Connect to contacts on the same Wi-Fi network + Connect to contacts via the Internet + All connections go through the Tor network for privacy + Connection method for Tor network Automatic based on location - Use Tor without bridges - Use Tor with bridges - Don\'t connect - + Use Tor network without bridges + Use Tor network with bridges + Don\'t connect to the Internet + Automatic: %1$s (in %2$s) Use mobile data - Connect via Internet (Tor) only when charging + Connect to the Internet only when charging Disables Internet connection when device is running on battery @@ -499,8 +493,6 @@ Sign out of Briar if a panic button is pressed Delete Account Delete your Briar account if a panic button is pressed. Caution: This will permanently delete your identities, contacts and messages - Uninstall Briar - This requires manual confirmation in a panic event Notifications diff --git a/briar-android/src/main/res/xml/settings.xml b/briar-android/src/main/res/xml/settings.xml index d45a80f86..21acdd98f 100644 --- a/briar-android/src/main/res/xml/settings.xml +++ b/briar-android/src/main/res/xml/settings.xml @@ -29,18 +29,34 @@ android:layout="@layout/preferences_category" android:title="@string/network_settings_title"> - + + + +