From 253544583145f0eed2f013036b000a2face650d7 Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Thu, 2 Aug 2018 11:39:06 +0100
Subject: [PATCH] Factor network management code out of plugins.

---
 .../bramble/BrambleAndroidModule.java         |   2 +
 .../network/AndroidNetworkManager.java        | 138 ++++++++++++++++++
 .../bramble/network/AndroidNetworkModule.java |  21 +++
 .../plugin/tcp/AndroidLanTcpPlugin.java       |  77 ++++------
 .../tcp/AndroidLanTcpPluginFactory.java       |  17 ++-
 .../bramble/plugin/tor/TorPlugin.java         |  94 ++++--------
 .../bramble/plugin/tor/TorPluginFactory.java  |  16 +-
 .../bramble/api/network/NetworkManager.java   |   6 +
 .../bramble/api/network/NetworkStatus.java    |  19 +++
 .../api/network/event/NetworkStatusEvent.java |  17 +++
 .../briarproject/briar/android/AppModule.java |  10 +-
 11 files changed, 280 insertions(+), 137 deletions(-)
 create mode 100644 bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java
 create mode 100644 bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkModule.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkManager.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java
 create mode 100644 bramble-api/src/main/java/org/briarproject/bramble/api/network/event/NetworkStatusEvent.java

diff --git a/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java b/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java
index cb563508ae..48c49794eb 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/BrambleAndroidModule.java
@@ -2,6 +2,7 @@ package org.briarproject.bramble;
 
 import android.app.Application;
 
+import org.briarproject.bramble.network.AndroidNetworkModule;
 import org.briarproject.bramble.plugin.tor.CircumventionProvider;
 import org.briarproject.bramble.plugin.tor.CircumventionProviderImpl;
 import org.briarproject.bramble.system.AndroidSystemModule;
@@ -12,6 +13,7 @@ import dagger.Module;
 import dagger.Provides;
 
 @Module(includes = {
+		AndroidNetworkModule.class,
 		AndroidSystemModule.class
 })
 public class BrambleAndroidModule {
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java b/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java
new file mode 100644
index 0000000000..fdc521be65
--- /dev/null
+++ b/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkManager.java
@@ -0,0 +1,138 @@
+package org.briarproject.bramble.network;
+
+import android.app.Application;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.lifecycle.Service;
+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.system.Scheduler;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static android.content.Context.CONNECTIVITY_SERVICE;
+import static android.content.Intent.ACTION_SCREEN_OFF;
+import static android.content.Intent.ACTION_SCREEN_ON;
+import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.logging.Level.INFO;
+
+class AndroidNetworkManager implements NetworkManager, Service {
+
+	private static final Logger LOG =
+			Logger.getLogger(AndroidNetworkManager.class.getName());
+
+	// See android.net.wifi.WifiManager
+	private static final String WIFI_AP_STATE_CHANGED_ACTION =
+			"android.net.wifi.WIFI_AP_STATE_CHANGED";
+	private static final int WIFI_AP_STATE_ENABLED = 13;
+
+	private final ScheduledExecutorService scheduler;
+	private final EventBus eventBus;
+	private final Context appContext;
+	private final AtomicReference<Future<?>> connectivityCheck =
+			new AtomicReference<>();
+	private final AtomicBoolean used = new AtomicBoolean(false);
+
+	private volatile BroadcastReceiver networkStateReceiver = null;
+
+	@Inject
+	AndroidNetworkManager(@Scheduler ScheduledExecutorService scheduler,
+			EventBus eventBus, Application app) {
+		this.scheduler = scheduler;
+		this.eventBus = eventBus;
+		this.appContext = app.getApplicationContext();
+	}
+
+	@Override
+	public void startService() {
+		if (used.getAndSet(true)) throw new IllegalStateException();
+		// Register to receive network status events
+		networkStateReceiver = new NetworkStateReceiver();
+		IntentFilter filter = new IntentFilter();
+		filter.addAction(CONNECTIVITY_ACTION);
+		filter.addAction(ACTION_SCREEN_ON);
+		filter.addAction(ACTION_SCREEN_OFF);
+		filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
+		if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
+		appContext.registerReceiver(networkStateReceiver, filter);
+
+	}
+
+	@Override
+	public void stopService() {
+		if (networkStateReceiver != null)
+			appContext.unregisterReceiver(networkStateReceiver);
+	}
+
+	@Override
+	public NetworkStatus getNetworkStatus() {
+		ConnectivityManager cm = (ConnectivityManager)
+				appContext.getSystemService(CONNECTIVITY_SERVICE);
+		assert cm != null;
+		NetworkInfo net = cm.getActiveNetworkInfo();
+		boolean connected = net != null && net.isConnected();
+		boolean wifi = connected && net.getType() == TYPE_WIFI;
+		return new NetworkStatus(connected, wifi);
+	}
+
+	private void updateConnectionStatus() {
+		eventBus.broadcast(new NetworkStatusEvent(getNetworkStatus()));
+	}
+
+	private void scheduleConnectionStatusUpdate(int delay, TimeUnit unit) {
+		Future<?> newConnectivityCheck =
+				scheduler.schedule(this::updateConnectionStatus, delay, unit);
+		Future<?> oldConnectivityCheck =
+				connectivityCheck.getAndSet(newConnectivityCheck);
+		if (oldConnectivityCheck != null) oldConnectivityCheck.cancel(false);
+	}
+
+	private class NetworkStateReceiver extends BroadcastReceiver {
+
+		@Override
+		public void onReceive(Context ctx, Intent i) {
+			String action = i.getAction();
+			if (LOG.isLoggable(INFO)) LOG.info("Received broadcast " + action);
+			updateConnectionStatus();
+			// TODO: Also schedule update after idle mode changes
+			if (isSleepEvent(i)) {
+				scheduleConnectionStatusUpdate(1, MINUTES);
+			} else if (isApEnabledEvent(i)) {
+				// The state change may be broadcast before the AP address is
+				// visible, so delay handling the event
+				// TODO: Wait longer, and also wait after stopping - see #1301
+				scheduleConnectionStatusUpdate(1, SECONDS);
+			}
+		}
+
+		private boolean isSleepEvent(Intent i) {
+			return ACTION_SCREEN_ON.equals(i.getAction()) ||
+					ACTION_SCREEN_OFF.equals(i.getAction());
+		}
+
+		private boolean isApEnabledEvent(Intent i) {
+			return WIFI_AP_STATE_CHANGED_ACTION.equals(i.getAction()) &&
+					i.getIntExtra(EXTRA_WIFI_STATE, 0) == WIFI_AP_STATE_ENABLED;
+		}
+	}
+}
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkModule.java b/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkModule.java
new file mode 100644
index 0000000000..b381b7a23a
--- /dev/null
+++ b/bramble-android/src/main/java/org/briarproject/bramble/network/AndroidNetworkModule.java
@@ -0,0 +1,21 @@
+package org.briarproject.bramble.network;
+
+import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.network.NetworkManager;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class AndroidNetworkModule {
+
+	@Provides
+	@Singleton
+	NetworkManager provideNetworkManager(LifecycleManager lifecycleManager,
+			AndroidNetworkManager networkManager) {
+		lifecycleManager.registerService(networkManager);
+		return networkManager;
+	}
+}
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 506bc997f0..7e1ef557a0 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,15 +1,16 @@
 package org.briarproject.bramble.plugin.tcp;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.net.ConnectivityManager;
 import android.net.Network;
 import android.net.NetworkInfo;
 import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 
+import org.briarproject.bramble.PoliteExecutor;
+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.duplex.DuplexPluginCallback;
@@ -20,7 +21,6 @@ import java.net.Socket;
 import java.net.UnknownHostException;
 import java.util.Collection;
 import java.util.concurrent.Executor;
-import java.util.concurrent.ScheduledExecutorService;
 import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
@@ -28,21 +28,13 @@ import javax.net.SocketFactory;
 
 import static android.content.Context.CONNECTIVITY_SERVICE;
 import static android.content.Context.WIFI_SERVICE;
-import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_WIFI;
-import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
 import static android.os.Build.VERSION.SDK_INT;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
-import static java.util.concurrent.TimeUnit.SECONDS;
 
 @NotNullByDefault
-class AndroidLanTcpPlugin extends LanTcpPlugin {
-
-	// See android.net.wifi.WifiManager
-	private static final String WIFI_AP_STATE_CHANGED_ACTION =
-			"android.net.wifi.WIFI_AP_STATE_CHANGED";
-	private static final int WIFI_AP_STATE_ENABLED = 13;
+class AndroidLanTcpPlugin extends LanTcpPlugin implements EventListener {
 
 	private static final byte[] WIFI_AP_ADDRESS_BYTES =
 			{(byte) 192, (byte) 168, 43, 1};
@@ -60,25 +52,23 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 		}
 	}
 
-	private final ScheduledExecutorService scheduler;
-	private final Context appContext;
+	private final Executor connectionStatusExecutor;
 	private final ConnectivityManager connectivityManager;
 	@Nullable
 	private final WifiManager wifiManager;
 
-	@Nullable
-	private volatile BroadcastReceiver networkStateReceiver = null;
 	private volatile SocketFactory socketFactory;
 
-	AndroidLanTcpPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
-			Backoff backoff, Context appContext, DuplexPluginCallback callback,
-			int maxLatency, int maxIdleTime) {
+	AndroidLanTcpPlugin(Executor ioExecutor, Context appContext,
+			Backoff backoff, DuplexPluginCallback callback, int maxLatency,
+			int maxIdleTime) {
 		super(ioExecutor, backoff, callback, maxLatency, maxIdleTime);
-		this.scheduler = scheduler;
-		this.appContext = appContext;
+		// Don't execute more than one connection status check at a time
+		connectionStatusExecutor =
+				new PoliteExecutor("AndroidLanTcpPlugin", ioExecutor, 1);
 		ConnectivityManager connectivityManager = (ConnectivityManager)
 				appContext.getSystemService(CONNECTIVITY_SERVICE);
-		if (connectivityManager == null) throw new AssertionError();
+		assert connectivityManager != null;
 		this.connectivityManager = connectivityManager;
 		wifiManager = (WifiManager) appContext.getApplicationContext()
 				.getSystemService(WIFI_SERVICE);
@@ -89,19 +79,12 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 	public void start() {
 		if (used.getAndSet(true)) throw new IllegalStateException();
 		running = true;
-		// Register to receive network status events
-		networkStateReceiver = new NetworkStateReceiver();
-		IntentFilter filter = new IntentFilter();
-		filter.addAction(CONNECTIVITY_ACTION);
-		filter.addAction(WIFI_AP_STATE_CHANGED_ACTION);
-		appContext.registerReceiver(networkStateReceiver, filter);
+		updateConnectionStatus();
 	}
 
 	@Override
 	public void stop() {
 		running = false;
-		if (networkStateReceiver != null)
-			appContext.unregisterReceiver(networkStateReceiver);
 		tryToClose(socket);
 	}
 
@@ -120,7 +103,7 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 			return singletonList(intToInetAddress(info.getIpAddress()));
 		// If we're running an access point, return its address
 		if (super.getLocalIpAddresses().contains(WIFI_AP_ADDRESS))
-				return singletonList(WIFI_AP_ADDRESS);
+			return singletonList(WIFI_AP_ADDRESS);
 		// No suitable addresses
 		return emptyList();
 	}
@@ -152,21 +135,16 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 		return SocketFactory.getDefault();
 	}
 
-	private class NetworkStateReceiver extends BroadcastReceiver {
-
-		@Override
-		public void onReceive(Context ctx, Intent i) {
-			if (!running) return;
-			if (isApEnabledEvent(i)) {
-				// The state change may be broadcast before the AP address is
-				// visible, so delay handling the event
-				scheduler.schedule(this::handleConnectivityChange, 1, SECONDS);
-			} else {
-				handleConnectivityChange();
-			}
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof NetworkStatusEvent) {
+			LOG.info("Network status changed");
+			updateConnectionStatus();
 		}
+	}
 
-		private void handleConnectivityChange() {
+	private void updateConnectionStatus() {
+		connectionStatusExecutor.execute(() -> {
 			if (!running) return;
 			Collection<InetAddress> addrs = getLocalIpAddresses();
 			if (addrs.contains(WIFI_AP_ADDRESS)) {
@@ -186,11 +164,6 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 				socketFactory = getSocketFactory();
 				if (socket == null || socket.isClosed()) bind();
 			}
-		}
-
-		private boolean isApEnabledEvent(Intent i) {
-			return WIFI_AP_STATE_CHANGED_ACTION.equals(i.getAction()) &&
-					i.getIntExtra(EXTRA_WIFI_STATE, 0) == WIFI_AP_STATE_ENABLED;
-		}
+		});
 	}
-}
+}
\ No newline at end of file
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java
index da8149c9a0..f8db864a07 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tcp/AndroidLanTcpPluginFactory.java
@@ -2,6 +2,7 @@ package org.briarproject.bramble.plugin.tcp;
 
 import android.content.Context;
 
+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;
@@ -11,7 +12,6 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
 
 import java.util.concurrent.Executor;
-import java.util.concurrent.ScheduledExecutorService;
 
 import javax.annotation.concurrent.Immutable;
 
@@ -28,15 +28,14 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
 	private static final double BACKOFF_BASE = 1.2;
 
 	private final Executor ioExecutor;
-	private final ScheduledExecutorService scheduler;
+	private final EventBus eventBus;
 	private final BackoffFactory backoffFactory;
 	private final Context appContext;
 
-	public AndroidLanTcpPluginFactory(Executor ioExecutor,
-			ScheduledExecutorService scheduler, BackoffFactory backoffFactory,
-			Context appContext) {
+	public AndroidLanTcpPluginFactory(Executor ioExecutor, EventBus eventBus,
+			BackoffFactory backoffFactory, Context appContext) {
 		this.ioExecutor = ioExecutor;
-		this.scheduler = scheduler;
+		this.eventBus = eventBus;
 		this.backoffFactory = backoffFactory;
 		this.appContext = appContext;
 	}
@@ -55,7 +54,9 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
 		Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
 				MAX_POLLING_INTERVAL, BACKOFF_BASE);
-		return new AndroidLanTcpPlugin(ioExecutor, scheduler, backoff,
-				appContext, callback, MAX_LATENCY, MAX_IDLE_TIME);
+		AndroidLanTcpPlugin plugin = new AndroidLanTcpPlugin(ioExecutor,
+				appContext, backoff, callback, MAX_LATENCY, MAX_IDLE_TIME);
+		eventBus.addListener(plugin);
+		return plugin;
 	}
 }
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
index 576ae75341..af96d2fc28 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPlugin.java
@@ -1,15 +1,10 @@
 package org.briarproject.bramble.plugin.tor;
 
-import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Resources;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
 import android.os.PowerManager;
 
 import net.freehaven.tor.control.EventHandler;
@@ -21,6 +16,9 @@ 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.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.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.Backoff;
@@ -59,10 +57,8 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Scanner;
 import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
 import java.util.logging.Logger;
 import java.util.regex.Pattern;
 import java.util.zip.ZipInputStream;
@@ -70,15 +66,8 @@ import java.util.zip.ZipInputStream;
 import javax.annotation.Nullable;
 import javax.net.SocketFactory;
 
-import static android.content.Context.CONNECTIVITY_SERVICE;
 import static android.content.Context.MODE_PRIVATE;
 import static android.content.Context.POWER_SERVICE;
-import static android.content.Intent.ACTION_SCREEN_OFF;
-import static android.content.Intent.ACTION_SCREEN_ON;
-import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
-import static android.net.ConnectivityManager.TYPE_WIFI;
-import static android.os.Build.VERSION.SDK_INT;
-import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
 import static android.os.PowerManager.PARTIAL_WAKE_LOCK;
 import static java.util.concurrent.TimeUnit.MINUTES;
 import static java.util.logging.Level.INFO;
@@ -113,8 +102,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 			Logger.getLogger(TorPlugin.class.getName());
 
 	private final Executor ioExecutor, connectionStatusExecutor;
-	private final ScheduledExecutorService scheduler;
 	private final Context appContext;
+	private final NetworkManager networkManager;
 	private final LocationUtils locationUtils;
 	private final SocketFactory torSocketFactory;
 	private final Clock clock;
@@ -127,24 +116,23 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	private final File torDirectory, torFile, geoIpFile, configFile;
 	private final File doneFile, cookieFile;
 	private final RenewableWakeLock wakeLock;
-	private final AtomicReference<Future<?>> connectivityCheck =
-			new AtomicReference<>();
 	private final AtomicBoolean used = new AtomicBoolean(false);
 
 	private volatile boolean running = false;
 	private volatile ServerSocket socket = null;
 	private volatile Socket controlSocket = null;
 	private volatile TorControlConnection controlConnection = null;
-	private volatile BroadcastReceiver networkStateReceiver = null;
 
 	TorPlugin(Executor ioExecutor, ScheduledExecutorService scheduler,
-			Context appContext, LocationUtils locationUtils,
-			SocketFactory torSocketFactory, Clock clock, Backoff backoff,
-			DuplexPluginCallback callback, String architecture,
-			CircumventionProvider circumventionProvider, int maxLatency, int maxIdleTime) {
+			Context appContext, NetworkManager networkManager,
+			LocationUtils locationUtils, SocketFactory torSocketFactory,
+			Clock clock, CircumventionProvider circumventionProvider,
+			Backoff backoff, DuplexPluginCallback callback,
+			String architecture,
+			int maxLatency, int maxIdleTime) {
 		this.ioExecutor = ioExecutor;
-		this.scheduler = scheduler;
 		this.appContext = appContext;
+		this.networkManager = networkManager;
 		this.locationUtils = locationUtils;
 		this.torSocketFactory = torSocketFactory;
 		this.clock = clock;
@@ -165,8 +153,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		doneFile = new File(torDirectory, "done");
 		cookieFile = new File(torDirectory, ".tor/control_auth_cookie");
 		// Don't execute more than one connection status check at a time
-		connectionStatusExecutor = new PoliteExecutor("TorPlugin",
-				ioExecutor, 1);
+		connectionStatusExecutor =
+				new PoliteExecutor("TorPlugin", ioExecutor, 1);
 		PowerManager pm = (PowerManager)
 				appContext.getSystemService(POWER_SERVICE);
 		wakeLock = new RenewableWakeLock(pm, scheduler, PARTIAL_WAKE_LOCK,
@@ -271,14 +259,8 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		} catch (IOException e) {
 			throw new PluginException(e);
 		}
-		// Register to receive network status events
-		networkStateReceiver = new NetworkStateReceiver();
-		IntentFilter filter = new IntentFilter();
-		filter.addAction(CONNECTIVITY_ACTION);
-		filter.addAction(ACTION_SCREEN_ON);
-		filter.addAction(ACTION_SCREEN_OFF);
-		if (SDK_INT >= 23) filter.addAction(ACTION_DEVICE_IDLE_MODE_CHANGED);
-		appContext.registerReceiver(networkStateReceiver, filter);
+		// Check whether we're online
+		updateConnectionStatus(networkManager.getNetworkStatus());
 		// Bind a server socket to receive incoming hidden service connections
 		bind();
 	}
@@ -517,8 +499,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	public void stop() {
 		running = false;
 		tryToClose(socket);
-		if (networkStateReceiver != null)
-			appContext.unregisterReceiver(networkStateReceiver);
 		if (controlSocket != null && controlConnection != null) {
 			try {
 				LOG.info("Stopping Tor");
@@ -628,8 +608,10 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	@Override
 	public void orConnStatus(String status, String orName) {
 		if (LOG.isLoggable(INFO)) LOG.info("OR connection " + status);
-		if (status.equals("CLOSED") || status.equals("FAILED"))
-			updateConnectionStatus(); // Check whether we've lost connectivity
+		if (status.equals("CLOSED") || status.equals("FAILED")) {
+			// Check whether we've lost connectivity
+			updateConnectionStatus(networkManager.getNetworkStatus());
+		}
 	}
 
 	@Override
@@ -662,19 +644,20 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 			SettingsUpdatedEvent s = (SettingsUpdatedEvent) e;
 			if (s.getNamespace().equals(ID.getString())) {
 				LOG.info("Tor settings updated");
-				updateConnectionStatus();
+				updateConnectionStatus(networkManager.getNetworkStatus());
 			}
+		} else if (e instanceof NetworkStatusEvent) {
+			LOG.info("Network status changed");
+			NetworkStatusEvent n = (NetworkStatusEvent) e;
+			updateConnectionStatus(n.getStatus());
 		}
 	}
 
-	private void updateConnectionStatus() {
+	private void updateConnectionStatus(NetworkStatus status) {
 		connectionStatusExecutor.execute(() -> {
 			if (!running) return;
-			Object o = appContext.getSystemService(CONNECTIVITY_SERVICE);
-			ConnectivityManager cm = (ConnectivityManager) o;
-			NetworkInfo net = cm.getActiveNetworkInfo();
-			boolean online = net != null && net.isConnected();
-			boolean wifi = online && net.getType() == TYPE_WIFI;
+			boolean online = status.isConnected();
+			boolean wifi = status.isWifi();
 			String country = locationUtils.getCurrentCountry();
 			boolean blocked =
 					circumventionProvider.isTorProbablyBlocked(country);
@@ -715,29 +698,6 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		});
 	}
 
-	private void scheduleConnectionStatusUpdate() {
-		Future<?> newConnectivityCheck =
-				scheduler.schedule(this::updateConnectionStatus, 1, MINUTES);
-		Future<?> oldConnectivityCheck =
-				connectivityCheck.getAndSet(newConnectivityCheck);
-		if (oldConnectivityCheck != null) oldConnectivityCheck.cancel(false);
-	}
-
-	private class NetworkStateReceiver extends BroadcastReceiver {
-
-		@Override
-		public void onReceive(Context ctx, Intent i) {
-			if (!running) return;
-			String action = i.getAction();
-			if (LOG.isLoggable(INFO)) LOG.info("Received broadcast " + action);
-			updateConnectionStatus();
-			if (ACTION_SCREEN_ON.equals(action)
-					|| ACTION_SCREEN_OFF.equals(action)) {
-				scheduleConnectionStatusUpdate();
-			}
-		}
-	}
-
 	private static class ConnectionStatus {
 
 		// All of the following are locking: this
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java
index a7feb23722..1055a03a7b 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/tor/TorPluginFactory.java
@@ -4,6 +4,7 @@ import android.content.Context;
 import android.os.Build;
 
 import org.briarproject.bramble.api.event.EventBus;
+import org.briarproject.bramble.api.network.NetworkManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.Backoff;
 import org.briarproject.bramble.api.plugin.BackoffFactory;
@@ -39,6 +40,7 @@ public class TorPluginFactory implements DuplexPluginFactory {
 	private final Executor ioExecutor;
 	private final ScheduledExecutorService scheduler;
 	private final Context appContext;
+	private final NetworkManager networkManager;
 	private final LocationUtils locationUtils;
 	private final EventBus eventBus;
 	private final SocketFactory torSocketFactory;
@@ -48,13 +50,14 @@ public class TorPluginFactory implements DuplexPluginFactory {
 
 	public TorPluginFactory(Executor ioExecutor,
 			ScheduledExecutorService scheduler, Context appContext,
-			LocationUtils locationUtils, EventBus eventBus,
-			SocketFactory torSocketFactory, BackoffFactory backoffFactory,
-			CircumventionProvider circumventionProvider,
-			Clock clock) {
+			NetworkManager networkManager, LocationUtils locationUtils,
+			EventBus eventBus, SocketFactory torSocketFactory,
+			BackoffFactory backoffFactory,
+			CircumventionProvider circumventionProvider, Clock clock) {
 		this.ioExecutor = ioExecutor;
 		this.scheduler = scheduler;
 		this.appContext = appContext;
+		this.networkManager = networkManager;
 		this.locationUtils = locationUtils;
 		this.eventBus = eventBus;
 		this.torSocketFactory = torSocketFactory;
@@ -97,8 +100,9 @@ public class TorPluginFactory implements DuplexPluginFactory {
 		Backoff backoff = backoffFactory.createBackoff(MIN_POLLING_INTERVAL,
 				MAX_POLLING_INTERVAL, BACKOFF_BASE);
 		TorPlugin plugin = new TorPlugin(ioExecutor, scheduler, appContext,
-				locationUtils, torSocketFactory, clock, backoff, callback,
-				architecture, circumventionProvider, MAX_LATENCY, MAX_IDLE_TIME);
+				networkManager, locationUtils, torSocketFactory, clock,
+				circumventionProvider, backoff, callback, architecture,
+				MAX_LATENCY, MAX_IDLE_TIME);
 		eventBus.addListener(plugin);
 		return plugin;
 	}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkManager.java
new file mode 100644
index 0000000000..5299c81267
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkManager.java
@@ -0,0 +1,6 @@
+package org.briarproject.bramble.api.network;
+
+public interface NetworkManager {
+
+	NetworkStatus getNetworkStatus();
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java b/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java
new file mode 100644
index 0000000000..af3f7f94d6
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/network/NetworkStatus.java
@@ -0,0 +1,19 @@
+package org.briarproject.bramble.api.network;
+
+public class NetworkStatus {
+
+	private final boolean connected, wifi;
+
+	public NetworkStatus(boolean connected, boolean wifi) {
+		this.connected = connected;
+		this.wifi = wifi;
+	}
+
+	public boolean isConnected() {
+		return connected;
+	}
+
+	public boolean isWifi() {
+		return wifi;
+	}
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/network/event/NetworkStatusEvent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/network/event/NetworkStatusEvent.java
new file mode 100644
index 0000000000..8ecf08ada1
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/network/event/NetworkStatusEvent.java
@@ -0,0 +1,17 @@
+package org.briarproject.bramble.api.network.event;
+
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.network.NetworkStatus;
+
+public class NetworkStatusEvent extends Event {
+
+	private final NetworkStatus status;
+
+	public NetworkStatusEvent(NetworkStatus status) {
+		this.status = status;
+	}
+
+	public NetworkStatus getStatus() {
+		return status;
+	}
+}
\ No newline at end of file
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 3af7162778..06d2757670 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
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.lifecycle.IoExecutor;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.network.NetworkManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.BackoffFactory;
 import org.briarproject.bramble.api.plugin.PluginConfig;
@@ -92,17 +93,18 @@ public class AppModule {
 			@Scheduler ScheduledExecutorService scheduler,
 			AndroidExecutor androidExecutor, SecureRandom random,
 			SocketFactory torSocketFactory, BackoffFactory backoffFactory,
-			Application app, LocationUtils locationUtils, EventBus eventBus,
+			Application app, NetworkManager networkManager,
+			LocationUtils locationUtils, EventBus eventBus,
 			CircumventionProvider circumventionProvider, Clock clock) {
 		Context appContext = app.getApplicationContext();
 		DuplexPluginFactory bluetooth =
 				new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor,
 						appContext, random, eventBus, backoffFactory);
 		DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, scheduler,
-				appContext, locationUtils, eventBus, torSocketFactory,
-				backoffFactory, circumventionProvider, clock);
+				appContext, networkManager, locationUtils, eventBus,
+				torSocketFactory, backoffFactory, circumventionProvider, clock);
 		DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
-				scheduler, backoffFactory, appContext);
+				eventBus, backoffFactory, appContext);
 		Collection<DuplexPluginFactory> duplex = asList(bluetooth, tor, lan);
 		@NotNullByDefault
 		PluginConfig pluginConfig = new PluginConfig() {
-- 
GitLab