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 a170cbccc02d67989685f6b6bb53eca7694a3d01..65e0211faa6bf3e90cf68790ab2c3522825fcd81 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
@@ -16,18 +16,27 @@ import org.briarproject.bramble.api.plugin.PluginException;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
 import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
 import org.briarproject.bramble.api.system.AndroidExecutor;
+import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.bramble.util.AndroidUtils;
 
 import java.io.Closeable;
 import java.io.IOException;
 import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
 import java.util.UUID;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
 import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 
+import static android.bluetooth.BluetoothAdapter.ACTION_DISCOVERY_FINISHED;
+import static android.bluetooth.BluetoothAdapter.ACTION_DISCOVERY_STARTED;
 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;
@@ -37,8 +46,13 @@ import static android.bluetooth.BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERA
 import static android.bluetooth.BluetoothAdapter.SCAN_MODE_NONE;
 import static android.bluetooth.BluetoothAdapter.STATE_OFF;
 import static android.bluetooth.BluetoothAdapter.STATE_ON;
+import static android.bluetooth.BluetoothDevice.ACTION_FOUND;
+import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.bramble.util.LogUtils.logException;
+import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
@@ -47,8 +61,11 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
 	private static final Logger LOG =
 			Logger.getLogger(AndroidBluetoothPlugin.class.getName());
 
+	private static final int MAX_DISCOVERY_MS = 10_000;
+
 	private final AndroidExecutor androidExecutor;
 	private final Context appContext;
+	private final Clock clock;
 
 	private volatile boolean wasEnabledByUs = false;
 	private volatile BluetoothStateReceiver receiver = null;
@@ -58,12 +75,13 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
 
 	AndroidBluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
 			Executor ioExecutor, AndroidExecutor androidExecutor,
-			Context appContext, SecureRandom secureRandom, Backoff backoff,
-			DuplexPluginCallback callback, int maxLatency) {
+			Context appContext, SecureRandom secureRandom, Clock clock,
+			Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
 		super(connectionLimiter, ioExecutor, secureRandom, backoff, callback,
 				maxLatency);
 		this.androidExecutor = androidExecutor;
 		this.appContext = appContext;
+		this.clock = clock;
 	}
 
 	@Override
@@ -182,6 +200,74 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
 		}
 	}
 
+	@Override
+	@Nullable
+	DuplexTransportConnection discoverAndConnect(String uuid) {
+		if (adapter == null) return null;
+		for (String address : discoverDevices()) {
+			try {
+				if (LOG.isLoggable(INFO))
+					LOG.info("Connecting to " + scrubMacAddress(address));
+				return connectTo(address, uuid);
+			} catch (IOException e) {
+				if (LOG.isLoggable(INFO)) {
+					LOG.info("Could not connect to "
+							+ scrubMacAddress(address));
+				}
+			}
+		}
+		LOG.info("Could not connect to any devices");
+		return null;
+	}
+
+	private Collection<String> discoverDevices() {
+		List<String> addresses = new ArrayList<>();
+		BlockingQueue<Intent> intents = new LinkedBlockingQueue<>();
+		DiscoveryReceiver receiver = new DiscoveryReceiver(intents);
+		IntentFilter filter = new IntentFilter();
+		filter.addAction(ACTION_DISCOVERY_STARTED);
+		filter.addAction(ACTION_DISCOVERY_FINISHED);
+		filter.addAction(ACTION_FOUND);
+		appContext.registerReceiver(receiver, filter);
+		try {
+			if (adapter.startDiscovery()) {
+				long now = clock.currentTimeMillis();
+				long end = now + MAX_DISCOVERY_MS;
+				while (now < end) {
+					Intent i = intents.poll(end - now, MILLISECONDS);
+					if (i == null) break;
+					String action = i.getAction();
+					if (ACTION_DISCOVERY_STARTED.equals(action)) {
+						LOG.info("Discovery started");
+					} else if (ACTION_DISCOVERY_FINISHED.equals(action)) {
+						LOG.info("Discovery finished");
+						break;
+					} else if (ACTION_FOUND.equals(action)) {
+						BluetoothDevice d = i.getParcelableExtra(EXTRA_DEVICE);
+						String address = d.getAddress();
+						if (LOG.isLoggable(INFO))
+							LOG.info("Discovered " + scrubMacAddress(address));
+						if (!addresses.contains(address))
+							addresses.add(address);
+					}
+					now = clock.currentTimeMillis();
+				}
+			} else {
+				LOG.info("Could not start discovery");
+			}
+		} catch (InterruptedException e) {
+			LOG.info("Interrupted while discovering devices");
+			Thread.currentThread().interrupt();
+		} finally {
+			LOG.info("Cancelling discovery");
+			adapter.cancelDiscovery();
+			appContext.unregisterReceiver(receiver);
+		}
+		// Shuffle the addresses so we don't always try the same one first
+		Collections.shuffle(addresses);
+		return addresses;
+	}
+
 	private void tryToClose(@Nullable Closeable c) {
 		try {
 			if (c != null) c.close();
@@ -207,4 +293,18 @@ class AndroidBluetoothPlugin extends BluetoothPlugin<BluetoothServerSocket> {
 			}
 		}
 	}
+
+	private static class DiscoveryReceiver extends BroadcastReceiver {
+
+		private final BlockingQueue<Intent> intents;
+
+		private DiscoveryReceiver(BlockingQueue<Intent> intents) {
+			this.intents = intents;
+		}
+
+		@Override
+		public void onReceive(Context ctx, Intent intent) {
+			intents.add(intent);
+		}
+	}
 }
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java
index c4b54673984080451b6b30a93c6aa851650aaa44..53a127caa71b8e9e2ffb627266414831093830c5 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/plugin/bluetooth/AndroidBluetoothPluginFactory.java
@@ -11,6 +11,7 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginCallback;
 import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
 import org.briarproject.bramble.api.system.AndroidExecutor;
+import org.briarproject.bramble.api.system.Clock;
 
 import java.security.SecureRandom;
 import java.util.concurrent.Executor;
@@ -33,17 +34,19 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
 	private final Context appContext;
 	private final SecureRandom secureRandom;
 	private final EventBus eventBus;
+	private final Clock clock;
 	private final BackoffFactory backoffFactory;
 
 	public AndroidBluetoothPluginFactory(Executor ioExecutor,
 			AndroidExecutor androidExecutor, Context appContext,
-			SecureRandom secureRandom, EventBus eventBus,
+			SecureRandom secureRandom, EventBus eventBus, Clock clock,
 			BackoffFactory backoffFactory) {
 		this.ioExecutor = ioExecutor;
 		this.androidExecutor = androidExecutor;
 		this.appContext = appContext;
 		this.secureRandom = secureRandom;
 		this.eventBus = eventBus;
+		this.clock = clock;
 		this.backoffFactory = backoffFactory;
 	}
 
@@ -65,7 +68,7 @@ public class AndroidBluetoothPluginFactory implements DuplexPluginFactory {
 				MAX_POLLING_INTERVAL, BACKOFF_BASE);
 		AndroidBluetoothPlugin plugin = new AndroidBluetoothPlugin(
 				connectionLimiter, ioExecutor, androidExecutor, appContext,
-				secureRandom, backoff, callback, MAX_LATENCY);
+				secureRandom, clock, backoff, callback, MAX_LATENCY);
 		eventBus.addListener(plugin);
 		return plugin;
 	}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java
index d329bff3fdcfb7f7331a4a4f8de7c4e9d9ec5ead..4041d0ce4a350f630f53ba4a15de4e2fa5d91a5f 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java
@@ -21,7 +21,7 @@ public interface KeyAgreementConstants {
 	/**
 	 * The connection timeout in milliseconds.
 	 */
-	long CONNECTION_TIMEOUT = 20 * 1000;
+	long CONNECTION_TIMEOUT = 60_000;
 
 	/**
 	 * The transport identifier for Bluetooth.
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 243548c0a8663424d74fe13cc75e41e4a640f6fc..0fdc03ceeabe9fe78dab4e813edc81539295ed42 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
@@ -23,7 +23,6 @@ import org.briarproject.bramble.api.plugin.event.EnableBluetoothEvent;
 import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.settings.Settings;
 import org.briarproject.bramble.api.settings.event.SettingsUpdatedEvent;
-import org.briarproject.bramble.util.StringUtils;
 
 import java.io.IOException;
 import java.security.SecureRandom;
@@ -46,6 +45,9 @@ 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.util.LogUtils.logException;
 import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
+import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
+import static org.briarproject.bramble.util.StringUtils.macToBytes;
+import static org.briarproject.bramble.util.StringUtils.macToString;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
@@ -96,6 +98,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 	abstract DuplexTransportConnection connectTo(String address, String uuid)
 			throws IOException;
 
+	@Nullable
+	abstract DuplexTransportConnection discoverAndConnect(String uuid);
+
 	BluetoothPlugin(BluetoothConnectionLimiter connectionLimiter,
 			Executor ioExecutor, SecureRandom secureRandom,
 			Backoff backoff, DuplexPluginCallback callback, int maxLatency) {
@@ -193,7 +198,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 			address = getBluetoothAddress();
 			if (LOG.isLoggable(INFO))
 				LOG.info("Local address " + scrubMacAddress(address));
-			if (!StringUtils.isNullOrEmpty(address)) {
+			if (!isNullOrEmpty(address)) {
 				p.put(PROP_ADDRESS, address);
 				changed = true;
 			}
@@ -256,9 +261,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 		// Try to connect to known devices in parallel
 		for (Entry<ContactId, TransportProperties> e : contacts.entrySet()) {
 			String address = e.getValue().get(PROP_ADDRESS);
-			if (StringUtils.isNullOrEmpty(address)) continue;
+			if (isNullOrEmpty(address)) continue;
 			String uuid = e.getValue().get(PROP_UUID);
-			if (StringUtils.isNullOrEmpty(uuid)) continue;
+			if (isNullOrEmpty(uuid)) continue;
 			ContactId c = e.getKey();
 			ioExecutor.execute(() -> {
 				if (!isRunning() || !shouldAllowContactConnections()) return;
@@ -309,9 +314,9 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 		if (!isRunning() || !shouldAllowContactConnections()) return null;
 		if (!connectionLimiter.canOpenContactConnection()) return null;
 		String address = p.get(PROP_ADDRESS);
-		if (StringUtils.isNullOrEmpty(address)) return null;
+		if (isNullOrEmpty(address)) return null;
 		String uuid = p.get(PROP_UUID);
-		if (StringUtils.isNullOrEmpty(uuid)) return null;
+		if (isNullOrEmpty(uuid)) return null;
 		DuplexTransportConnection conn = connect(address, uuid);
 		if (conn == null) return null;
 		// TODO: Why don't we reset the backoff here?
@@ -326,9 +331,6 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 	@Override
 	public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
 		if (!isRunning()) return null;
-		// There's no point listening if we can't discover our own address
-		String address = getBluetoothAddress();
-		if (address == null) 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);
@@ -346,7 +348,8 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 		}
 		BdfList descriptor = new BdfList();
 		descriptor.add(TRANSPORT_ID_BLUETOOTH);
-		descriptor.add(StringUtils.macToBytes(address));
+		String address = getBluetoothAddress();
+		if (address != null) descriptor.add(macToBytes(address));
 		return new BluetoothKeyAgreementListener(descriptor, ss);
 	}
 
@@ -354,18 +357,25 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 	public DuplexTransportConnection createKeyAgreementConnection(
 			byte[] commitment, BdfList descriptor) {
 		if (!isRunning()) return null;
-		String address;
-		try {
-			address = parseAddress(descriptor);
-		} catch (FormatException e) {
-			LOG.info("Invalid address in key agreement descriptor");
-			return null;
-		}
 		// No truncation necessary because COMMIT_LENGTH = 16
 		String uuid = UUID.nameUUIDFromBytes(commitment).toString();
-		if (LOG.isLoggable(INFO))
-			LOG.info("Connecting to key agreement UUID " + uuid);
-		DuplexTransportConnection conn = connect(address, uuid);
+		DuplexTransportConnection conn;
+		if (descriptor.size() == 1) {
+			if (LOG.isLoggable(INFO))
+				LOG.info("Discovering address for key agreement UUID " + uuid);
+			conn = discoverAndConnect(uuid);
+		} else {
+			String address;
+			try {
+				address = parseAddress(descriptor);
+			} catch (FormatException e) {
+				LOG.info("Invalid address in key agreement descriptor");
+				return null;
+			}
+			if (LOG.isLoggable(INFO))
+				LOG.info("Connecting to key agreement UUID " + uuid);
+			conn = connect(address, uuid);
+		}
 		if (conn != null) connectionLimiter.keyAgreementConnectionOpened(conn);
 		return conn;
 	}
@@ -373,7 +383,7 @@ abstract class BluetoothPlugin<SS> implements DuplexPlugin, EventListener {
 	private String parseAddress(BdfList descriptor) throws FormatException {
 		byte[] mac = descriptor.getRaw(1);
 		if (mac.length != 6) throw new FormatException();
-		return StringUtils.macToString(mac);
+		return macToString(mac);
 	}
 
 	@Override
diff --git a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java
index c3652bfb9c93f91c891591dbaaaac0e9a758def2..c3fe9b028763cc43fd47db6efb551de6f49583eb 100644
--- a/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java
+++ b/bramble-java/src/main/java/org/briarproject/bramble/plugin/bluetooth/JavaBluetoothPlugin.java
@@ -108,6 +108,12 @@ class JavaBluetoothPlugin extends BluetoothPlugin<StreamConnectionNotifier> {
 		return wrapSocket((StreamConnection) Connector.open(url));
 	}
 
+	@Override
+	@Nullable
+	DuplexTransportConnection discoverAndConnect(String uuid) {
+		return null; // TODO
+	}
+
 	private String makeUrl(String address, String uuid) {
 		return "btspp://" + address + ":" + uuid + ";name=RFCOMM";
 	}
diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml
index 885429cc2dc610390ac8c490b24eba605dbe9865..b5a63032d66e2187c30c247ae22f9115982cdb2d 100644
--- a/briar-android/src/main/AndroidManifest.xml
+++ b/briar-android/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
 	<uses-feature android:name="android.hardware.camera" android:required="false"/>
 	<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
 
+	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
 	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 	<uses-permission android:name="android.permission.BLUETOOTH" />
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 606647958d59240504e2bd1f8e553edc13f538b1..bbfb919cee953561eed6e554a134b88391282ea7 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
@@ -107,7 +107,7 @@ public class AppModule {
 		Context appContext = app.getApplicationContext();
 		DuplexPluginFactory bluetooth =
 				new AndroidBluetoothPluginFactory(ioExecutor, androidExecutor,
-						appContext, random, eventBus, backoffFactory);
+						appContext, random, eventBus, clock, backoffFactory);
 		DuplexPluginFactory tor = new AndroidTorPluginFactory(ioExecutor,
 				scheduler, appContext, networkManager, locationUtils, eventBus,
 				torSocketFactory, backoffFactory, resourceProvider,
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java
index f3d72d17cd44b085212b4d65e3d4e20e21133f40..5d087d88b1866bb0d74f695434a6cfffb5d0ee90 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java
@@ -9,9 +9,9 @@ public interface RequestCodes {
 	int REQUEST_WRITE_BLOG_POST = 5;
 	int REQUEST_SHARE_BLOG = 6;
 	int REQUEST_RINGTONE = 7;
-	int REQUEST_PERMISSION_CAMERA = 8;
+	int REQUEST_PERMISSION_CAMERA_LOCATION = 8;
 	int REQUEST_DOZE_WHITELISTING = 9;
-	int REQUEST_ENABLE_BLUETOOTH = 10;
+	int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
 	int REQUEST_UNLOCK = 11;
 	int REQUEST_KEYGUARD_UNLOCK = 12;
 
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 d74c3e24886713d45181dfe6bd825a19450ed89a..4298e89c615126733a6fec47199e9f38b32a245e 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
@@ -3,7 +3,6 @@ package org.briarproject.briar.android.keyagreement;
 import android.bluetooth.BluetoothAdapter;
 import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.os.Bundle;
@@ -11,19 +10,15 @@ import android.support.annotation.StringRes;
 import android.support.annotation.UiThread;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.FragmentManager;
-import android.support.v4.content.ContextCompat;
 import android.support.v7.app.AlertDialog.Builder;
 import android.support.v7.widget.Toolbar;
 import android.view.MenuItem;
-import android.widget.Toast;
 
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.event.BluetoothEnabledEvent;
 import org.briarproject.briar.R;
-import org.briarproject.briar.R.string;
-import org.briarproject.briar.R.style;
 import org.briarproject.briar.android.activity.ActivityComponent;
 import org.briarproject.briar.android.activity.BriarActivity;
 import org.briarproject.briar.android.fragment.BaseFragment;
@@ -37,16 +32,19 @@ import java.util.logging.Logger;
 import javax.annotation.Nullable;
 import javax.inject.Inject;
 
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
 import static android.Manifest.permission.CAMERA;
-import static android.bluetooth.BluetoothAdapter.ACTION_REQUEST_ENABLE;
+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 android.os.Build.VERSION.SDK_INT;
-import static android.widget.Toast.LENGTH_LONG;
-import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ENABLE_BLUETOOTH;
-import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA;
+import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_BLUETOOTH_DISCOVERABLE;
+import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_PERMISSION_CAMERA_LOCATION;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
@@ -55,7 +53,11 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		KeyAgreementEventListener {
 
 	private enum BluetoothState {
-		UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED
+		UNKNOWN, NO_ADAPTER, WAITING, REFUSED, ENABLED, DISCOVERABLE
+	}
+
+	private enum Permission {
+		UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
 	}
 
 	private static final Logger LOG =
@@ -64,8 +66,27 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 	@Inject
 	EventBus eventBus;
 
-	private boolean isResumed = false, enableWasRequested = false;
-	private boolean continueClicked, gotCameraPermission;
+	/**
+	 * Set to true in onPostResume() and false in onPause(). This prevents the
+	 * QR code fragment from being shown if onRequestPermissionsResult() is
+	 * called while the activity is paused, which could cause a crash due to
+	 * 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;
+	private Permission cameraPermission = Permission.UNKNOWN;
+	private Permission locationPermission = Permission.UNKNOWN;
 	private BluetoothState bluetoothState = BluetoothState.UNKNOWN;
 	private BroadcastReceiver bluetoothReceiver = null;
 
@@ -85,7 +106,9 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		if (state == null) {
 			showInitialFragment(IntroFragment.newInstance());
 		}
-		IntentFilter filter = new IntentFilter(ACTION_STATE_CHANGED);
+		IntentFilter filter = new IntentFilter();
+		filter.addAction(ACTION_STATE_CHANGED);
+		filter.addAction(ACTION_SCAN_MODE_CHANGED);
 		bluetoothReceiver = new BluetoothStateReceiver();
 		registerReceiver(bluetoothReceiver, filter);
 	}
@@ -107,20 +130,40 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 		}
 	}
 
+	@Override
+	public void onStart() {
+		super.onStart();
+		// Permissions may have been granted manually while we were stopped
+		cameraPermission = Permission.UNKNOWN;
+		locationPermission = Permission.UNKNOWN;
+	}
+
 	@Override
 	protected void onPostResume() {
 		super.onPostResume();
 		isResumed = true;
 		// Workaround for
 		// https://code.google.com/p/android/issues/detail?id=190966
-		if (canShowQrCodeFragment()) showQrCodeFragment();
+		showQrCodeFragmentIfAllowed();
+	}
+
+	private void showQrCodeFragmentIfAllowed() {
+		if (isResumed && continueClicked && areEssentialPermissionsGranted()) {
+			if (bluetoothState == BluetoothState.UNKNOWN ||
+					bluetoothState == BluetoothState.ENABLED) {
+				requestBluetoothDiscoverable();
+			} else if (bluetoothState != BluetoothState.WAITING) {
+				showQrCodeFragment();
+			}
+		}
 	}
 
-	private boolean canShowQrCodeFragment() {
-		return isResumed && continueClicked
-				&& (SDK_INT < 23 || gotCameraPermission)
-				&& bluetoothState != BluetoothState.UNKNOWN
-				&& bluetoothState != BluetoothState.WAITING;
+	private boolean areEssentialPermissionsGranted() {
+		// If the camera permission has been granted, and the location
+		// permission has been granted or permanently denied, we can continue
+		return cameraPermission == Permission.GRANTED &&
+				(locationPermission == Permission.GRANTED ||
+						locationPermission == Permission.PERMANENTLY_DENIED);
 	}
 
 	@Override
@@ -132,50 +175,54 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 	@Override
 	public void showNextScreen() {
 		continueClicked = true;
-		if (checkPermissions()) {
-			if (shouldRequestEnableBluetooth()) requestEnableBluetooth();
-			else if (canShowQrCodeFragment()) showQrCodeFragment();
-		}
-	}
-
-	private boolean shouldRequestEnableBluetooth() {
-		return bluetoothState == BluetoothState.UNKNOWN
-				|| bluetoothState == BluetoothState.REFUSED;
+		if (checkPermissions()) showQrCodeFragmentIfAllowed();
 	}
 
-	private void requestEnableBluetooth() {
+	private void requestBluetoothDiscoverable() {
 		BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
 		if (bt == null) {
 			setBluetoothState(BluetoothState.NO_ADAPTER);
-		} else if (bt.isEnabled()) {
-			setBluetoothState(BluetoothState.ENABLED);
 		} else {
-			enableWasRequested = true;
 			setBluetoothState(BluetoothState.WAITING);
-			Intent i = new Intent(ACTION_REQUEST_ENABLE);
-			startActivityForResult(i, REQUEST_ENABLE_BLUETOOTH);
+			wasAdapterEnabled = bt.isEnabled();
+			Intent i = new Intent(ACTION_REQUEST_DISCOVERABLE);
+			startActivityForResult(i, REQUEST_BLUETOOTH_DISCOVERABLE);
 		}
 	}
 
 	private void setBluetoothState(BluetoothState bluetoothState) {
 		LOG.info("Setting Bluetooth state to " + bluetoothState);
 		this.bluetoothState = bluetoothState;
-		if (enableWasRequested && bluetoothState == BluetoothState.ENABLED) {
+		if (!wasAdapterEnabled && bluetoothState == BluetoothState.ENABLED) {
 			eventBus.broadcast(new BluetoothEnabledEvent());
-			enableWasRequested = false;
+			wasAdapterEnabled = true;
 		}
-		if (canShowQrCodeFragment()) showQrCodeFragment();
+		showQrCodeFragmentIfAllowed();
 	}
 
 	@Override
 	public void onActivityResult(int request, int result, Intent data) {
-		// If the request was granted we'll catch the state change event
-		if (request == REQUEST_ENABLE_BLUETOOTH && result == RESULT_CANCELED)
-			setBluetoothState(BluetoothState.REFUSED);
+		if (request == REQUEST_BLUETOOTH_DISCOVERABLE) {
+			if (result == RESULT_CANCELED) {
+				setBluetoothState(BluetoothState.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);
+			}
+		}
 	}
 
 	private void showQrCodeFragment() {
+		// If we return to the intro fragment, the continue button needs to be
+		// clicked again before showing the QR code fragment
 		continueClicked = false;
+		// If we return to the intro fragment, ask for Bluetooth
+		// discoverability again before showing the QR code fragment
+		bluetoothState = BluetoothState.UNKNOWN;
 		// FIXME #824
 		FragmentManager fm = getSupportFragmentManager();
 		if (fm.findFragmentByTag(KeyAgreementFragment.TAG) == null) {
@@ -194,74 +241,113 @@ public abstract class KeyAgreementActivity extends BriarActivity implements
 	}
 
 	private boolean checkPermissions() {
-		if (ContextCompat.checkSelfPermission(this, CAMERA) !=
-				PERMISSION_GRANTED) {
-			// Should we show an explanation?
-			if (ActivityCompat.shouldShowRequestPermissionRationale(this,
-					CAMERA)) {
-				OnClickListener continueListener =
-						(dialog, which) -> requestPermission();
-				Builder builder = new Builder(this, style.BriarDialogTheme);
-				builder.setTitle(string.permission_camera_title);
-				builder.setMessage(string.permission_camera_request_body);
-				builder.setNeutralButton(string.continue_button,
-						continueListener);
-				builder.show();
-			} else {
-				requestPermission();
-			}
-			gotCameraPermission = false;
+		if (areEssentialPermissionsGranted()) return true;
+		// If the camera permission has been permanently denied, ask the
+		// user to change the setting
+		if (cameraPermission == Permission.PERMANENTLY_DENIED) {
+			Builder builder = new Builder(this, R.style.BriarDialogTheme);
+			builder.setTitle(R.string.permission_camera_title);
+			builder.setMessage(R.string.permission_camera_denied_body);
+			builder.setPositiveButton(R.string.ok,
+					UiUtils.getGoToSettingsListener(this));
+			builder.setNegativeButton(R.string.cancel,
+					(dialog, which) -> supportFinishAfterTransition());
+			builder.show();
 			return false;
+		}
+		// Should we show the rationale for one or both permissions?
+		if (cameraPermission == Permission.SHOW_RATIONALE &&
+				locationPermission == Permission.SHOW_RATIONALE) {
+			showRationale(R.string.permission_camera_location_title,
+					R.string.permission_camera_location_request_body);
+		} else if (cameraPermission == Permission.SHOW_RATIONALE) {
+			showRationale(R.string.permission_camera_title,
+					R.string.permission_camera_request_body);
+		} else if (locationPermission == Permission.SHOW_RATIONALE) {
+			showRationale(R.string.permission_location_title,
+					R.string.permission_location_request_body);
 		} else {
-			gotCameraPermission = true;
-			return true;
+			requestPermissions();
 		}
+		return false;
+	}
+
+	private void showRationale(@StringRes int title, @StringRes int body) {
+		Builder builder = new Builder(this, R.style.BriarDialogTheme);
+		builder.setTitle(title);
+		builder.setMessage(body);
+		builder.setNeutralButton(R.string.continue_button,
+				(dialog, which) -> requestPermissions());
+		builder.show();
 	}
 
-	private void requestPermission() {
-		ActivityCompat.requestPermissions(this, new String[] {CAMERA},
-				REQUEST_PERMISSION_CAMERA);
+	private void requestPermissions() {
+		ActivityCompat.requestPermissions(this,
+				new String[] {CAMERA, ACCESS_COARSE_LOCATION},
+				REQUEST_PERMISSION_CAMERA_LOCATION);
 	}
 
 	@Override
 	@UiThread
 	public void onRequestPermissionsResult(int requestCode,
-			String permissions[], int[] grantResults) {
-		if (requestCode == REQUEST_PERMISSION_CAMERA) {
-			// If request is cancelled, the result arrays are empty.
-			if (grantResults.length > 0 &&
-					grantResults[0] == PERMISSION_GRANTED) {
-				gotCameraPermission = true;
-				showNextScreen();
-			} else {
-				if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
-						CAMERA)) {
-					// The user has permanently denied the request
-					OnClickListener cancelListener =
-							(dialog, which) -> supportFinishAfterTransition();
-					Builder builder = new Builder(this, style.BriarDialogTheme);
-					builder.setTitle(string.permission_camera_title);
-					builder.setMessage(string.permission_camera_denied_body);
-					builder.setPositiveButton(string.ok,
-							UiUtils.getGoToSettingsListener(this));
-					builder.setNegativeButton(string.cancel, cancelListener);
-					builder.show();
-				} else {
-					Toast.makeText(this, string.permission_camera_denied_toast,
-							LENGTH_LONG).show();
-					supportFinishAfterTransition();
-				}
-			}
+			String[] permissions, int[] grantResults) {
+		if (requestCode != REQUEST_PERMISSION_CAMERA_LOCATION)
+			throw new AssertionError();
+		if (gotPermission(CAMERA, permissions, grantResults)) {
+			cameraPermission = Permission.GRANTED;
+		} else if (shouldShowRationale(CAMERA)) {
+			cameraPermission = Permission.SHOW_RATIONALE;
+		} else {
+			cameraPermission = Permission.PERMANENTLY_DENIED;
 		}
+		if (gotPermission(ACCESS_COARSE_LOCATION, permissions, grantResults)) {
+			locationPermission = Permission.GRANTED;
+		} else if (shouldShowRationale(ACCESS_COARSE_LOCATION)) {
+			locationPermission = Permission.SHOW_RATIONALE;
+		} else {
+			locationPermission = Permission.PERMANENTLY_DENIED;
+		}
+		// If a permission dialog has been shown, showing the QR code fragment
+		// on this call path would cause a crash due to
+		// https://code.google.com/p/android/issues/detail?id=190966.
+		// In that case the isResumed flag prevents the fragment from being
+		// shown here, and showQrCodeFragmentIfAllowed() will be called again
+		// from onPostResume().
+		if (checkPermissions()) showQrCodeFragmentIfAllowed();
+	}
+
+	private boolean gotPermission(String permission, String[] permissions,
+			int[] grantResults) {
+		for (int i = 0; i < permissions.length; i++) {
+			if (permission.equals(permissions[i]))
+				return grantResults[i] == PERMISSION_GRANTED;
+		}
+		return false;
+	}
+
+	private boolean shouldShowRationale(String permission) {
+		return ActivityCompat.shouldShowRequestPermissionRationale(this,
+				permission);
 	}
 
 	private class BluetoothStateReceiver extends BroadcastReceiver {
 
 		@Override
 		public void onReceive(Context context, Intent intent) {
-			int state = intent.getIntExtra(EXTRA_STATE, 0);
-			if (state == STATE_ON) setBluetoothState(BluetoothState.ENABLED);
-			else setBluetoothState(BluetoothState.UNKNOWN);
+			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);
+			}
 		}
 	}
 }
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 4875cc50c4b2c93205185adcfd32e2a579eb4c55..b67f0c25606bbf03e5f275ae438ca92a521480ab 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -468,8 +468,11 @@
 	<!-- Permission Requests -->
 	<string name="permission_camera_title">Camera permission</string>
 	<string name="permission_camera_request_body">To scan the QR code, Briar needs access to the camera.</string>
+	<string name="permission_location_title">Location permission</string>
+	<string name="permission_location_request_body">To discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string>
+	<string name="permission_camera_location_title">Camera and location</string>
+	<string name="permission_camera_location_request_body">To scan the QR code, Briar needs access to the camera.\n\nTo discover Bluetooth devices, Briar needs permission to access your location.\n\nBriar does not store your location or share it with anyone.</string>
 	<string name="permission_camera_denied_body">You have denied access to the camera, but adding contacts requires using the camera.\n\nPlease consider granting access.</string>
-	<string name="permission_camera_denied_toast">Camera permission was not granted</string>
 	<string name="qr_code">QR code</string>
 	<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>