diff --git a/bramble-android/src/main/AndroidManifest.xml b/bramble-android/src/main/AndroidManifest.xml
index 9a4bd9810c28b97b47a029d928d2590c669e904e..d1aebe349908420397b42fffec1f1c2f1d836887 100644
--- a/bramble-android/src/main/AndroidManifest.xml
+++ b/bramble-android/src/main/AndroidManifest.xml
@@ -1,15 +1,25 @@
-<manifest
-	package="org.briarproject.bramble"
-	xmlns:android="http://schemas.android.com/apk/res/android">
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+	package="org.briarproject.bramble">
 
-	<uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
+	<uses-feature
+		android:name="android.hardware.bluetooth"
+		android:required="false" />
 
-	<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"/>
-	<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-	<uses-permission android:name="android.permission.INTERNET"/>
-	<uses-permission android:name="android.permission.WAKE_LOCK"/>
+	<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"
+		android:maxSdkVersion="30" />
+	<uses-permission
+		android:name="android.permission.BLUETOOTH_ADMIN"
+		android:maxSdkVersion="30" />
+	<uses-permission
+		android:name="android.permission.BLUETOOTH_SCAN"
+		android:usesPermissionFlags="neverForLocation" />
+	<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+	<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+	<uses-permission android:name="android.permission.INTERNET" />
+	<uses-permission android:name="android.permission.WAKE_LOCK" />
 
 	<application
 		android:allowBackup="false"
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 70f0229cf9a26371629d3417feb8b242d06f8896..f2d8c02ad1bad892405961bac37628521d66a06e 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
@@ -55,6 +55,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
 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.AndroidUtils.hasBtConnectPermission;
 import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
 
 @MethodsNotNullByDefault
@@ -97,6 +98,11 @@ class AndroidBluetoothPlugin extends
 		this.clock = clock;
 	}
 
+	@Override
+	protected boolean isBluetoothAccessible() {
+		return hasBtConnectPermission(app);
+	}
+
 	@Override
 	public void start() throws PluginException {
 		super.start();
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java b/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java
index 0f7ad28351cc93754d569553bdcfbbe1b8cf12ff..b6c0c87da12c11a55d11d6edb735012e38495e14 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/system/AndroidSecureRandomProvider.java
@@ -6,7 +6,6 @@ import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.content.ContentResolver;
 import android.content.Context;
-import android.os.Build;
 import android.os.Parcel;
 import android.os.StrictMode;
 import android.provider.Settings;
@@ -15,12 +14,19 @@ import org.briarproject.nullsafety.NotNullByDefault;
 
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.util.Set;
 
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
+import static android.os.Build.FINGERPRINT;
+import static android.os.Build.SERIAL;
 import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Process.myPid;
+import static android.os.Process.myTid;
+import static android.os.Process.myUid;
 import static android.provider.Settings.Secure.ANDROID_ID;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
 
 @Immutable
 @NotNullByDefault
@@ -39,22 +45,27 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
 	@Override
 	protected void writeToEntropyPool(DataOutputStream out) throws IOException {
 		super.writeToEntropyPool(out);
-		out.writeInt(android.os.Process.myPid());
-		out.writeInt(android.os.Process.myTid());
-		out.writeInt(android.os.Process.myUid());
-		if (Build.FINGERPRINT != null) out.writeUTF(Build.FINGERPRINT);
-		if (Build.SERIAL != null) out.writeUTF(Build.SERIAL);
+		out.writeInt(myPid());
+		out.writeInt(myTid());
+		out.writeInt(myUid());
+		if (FINGERPRINT != null) out.writeUTF(FINGERPRINT);
+		if (SERIAL != null) out.writeUTF(SERIAL);
 		ContentResolver contentResolver = appContext.getContentResolver();
 		String id = Settings.Secure.getString(contentResolver, ANDROID_ID);
 		if (id != null) out.writeUTF(id);
-		Parcel parcel = Parcel.obtain();
-		BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
-		if (bt != null) {
-			for (BluetoothDevice device : bt.getBondedDevices())
-				parcel.writeParcelable(device, 0);
+		// use bluetooth paired devices as well, if allowed
+		if (hasBtConnectPermission(appContext)) {
+			Parcel parcel = Parcel.obtain();
+			BluetoothAdapter bt = BluetoothAdapter.getDefaultAdapter();
+			if (bt != null) {
+				@SuppressLint("MissingPermission")
+				Set<BluetoothDevice> deviceSet = bt.getBondedDevices();
+				for (BluetoothDevice device : deviceSet)
+					parcel.writeParcelable(device, 0);
+			}
+			out.write(parcel.marshall());
+			parcel.recycle();
 		}
-		out.write(parcel.marshall());
-		parcel.recycle();
 	}
 
 	@Override
@@ -77,7 +88,7 @@ class AndroidSecureRandomProvider extends UnixSecureRandomProvider {
 					.invoke(null, (Object) seed);
 			// Mix the output of the Linux PRNG into the OpenSSL PRNG
 			int bytesRead = (Integer) Class.forName(
-					"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+							"org.apache.harmony.xnet.provider.jsse.NativeCrypto")
 					.getMethod("RAND_load_file", String.class, long.class)
 					.invoke(null, "/dev/urandom", 1024);
 			if (bytesRead != 1024) throw new IOException();
diff --git a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java
index 6fe18e08094e78a2b6aa61c525ceb908f6ca910a..ffb4216ccbbaaf6a9d33fb968106bef131008508 100644
--- a/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java
+++ b/bramble-android/src/main/java/org/briarproject/bramble/util/AndroidUtils.java
@@ -22,9 +22,14 @@ import java.util.Scanner;
 
 import javax.annotation.Nullable;
 
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_SCAN;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.content.Context.MODE_PRIVATE;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Process.myPid;
+import static android.os.Process.myUid;
 import static java.lang.Runtime.getRuntime;
 import static java.util.Arrays.asList;
 import static org.briarproject.nullsafety.NullSafety.requireNonNull;
@@ -49,6 +54,16 @@ public class AndroidUtils {
 		return abis;
 	}
 
+	public static boolean hasBtScanPermission(Context ctx) {
+		return SDK_INT < 31 || ctx.checkPermission(BLUETOOTH_SCAN, myPid(),
+				myUid()) == PERMISSION_GRANTED;
+	}
+
+	public static boolean hasBtConnectPermission(Context ctx) {
+		return SDK_INT < 31 || ctx.checkPermission(BLUETOOTH_CONNECT, myPid(),
+				myUid()) == PERMISSION_GRANTED;
+	}
+
 	public static String getBluetoothAddress(Context ctx,
 			BluetoothAdapter adapter) {
 		return getBluetoothAddressAndMethod(ctx, adapter).getFirst();
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java
index 374c10d4f450de7a3f41b337738286a0c7b61d84..18a9be8947871dc1d9b46570821b7de0e89ceeb4 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/plugin/bluetooth/AbstractBluetoothPlugin.java
@@ -89,6 +89,16 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
 
 	private volatile String contactConnectionsUuid = null;
 
+	/**
+	 * Override and return true, if the plugin is now allowed to access the
+	 * Bluetooth hardware, so it must be
+	 * {@link org.briarproject.bramble.api.plugin.Plugin.State#DISABLED}
+	 * in {@link #start()}.
+	 */
+	protected boolean isBluetoothAccessible() {
+		return true;
+	}
+
 	abstract void initialiseAdapter() throws IOException;
 
 	abstract boolean isAdapterEnabled();
@@ -176,19 +186,28 @@ abstract class AbstractBluetoothPlugin<S, SS> implements BluetoothPlugin,
 				DEFAULT_PREF_PLUGIN_ENABLE);
 		everConnected.set(settings.getBoolean(PREF_EVER_CONNECTED,
 				DEFAULT_PREF_EVER_CONNECTED));
+		// disable plugin, if conditions for enabling are not met
+		if (enabledByUser && !isBluetoothAccessible()) {
+			enabledByUser = false;
+			settings.putBoolean(PREF_PLUGIN_ENABLE, false);
+			callback.mergeSettings(settings);
+		}
 		state.setStarted(enabledByUser);
 		try {
 			initialiseAdapter();
 		} catch (IOException e) {
 			throw new PluginException(e);
 		}
-		updateProperties();
-		if (enabledByUser && isAdapterEnabled()) bind();
+		if (enabledByUser) {
+			updateProperties();
+			if (isAdapterEnabled()) bind();
+		}
 	}
 
 	private void bind() {
 		ioExecutor.execute(() -> {
 			if (getState() != INACTIVE) return;
+			if (contactConnectionsUuid == null) updateProperties();
 			// Bind a server socket to accept connections from contacts
 			SS ss;
 			try {
diff --git a/briar-android/src/main/AndroidManifest.xml b/briar-android/src/main/AndroidManifest.xml
index 3b67935d48c1dee2ba9816df5cb6e5fd028d4d3b..3518fe872eecd8e0c8052f584b910b137378dc81 100644
--- a/briar-android/src/main/AndroidManifest.xml
+++ b/briar-android/src/main/AndroidManifest.xml
@@ -19,8 +19,6 @@
 	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 	<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 	<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
-	<uses-permission android:name="android.permission.BLUETOOTH" />
-	<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
 	<uses-permission android:name="android.permission.CAMERA" />
 	<uses-permission android:name="android.permission.INTERNET" />
 	<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
index f40dbe094890a35cc5cd0cea9c9c5abb2d9bd494..3ff872c64a07625c131e8cbcc6a3c54b9db5815c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactPermissionManager.java
@@ -11,20 +11,31 @@ import androidx.core.util.Consumer;
 import androidx.fragment.app.FragmentActivity;
 
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_SCAN;
 import static android.Manifest.permission.CAMERA;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Build.VERSION.SDK_INT;
 import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
 import static androidx.core.content.ContextCompat.checkSelfPermission;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
+import static org.briarproject.briar.android.util.Permission.GRANTED;
+import static org.briarproject.briar.android.util.Permission.PERMANENTLY_DENIED;
+import static org.briarproject.briar.android.util.Permission.SHOW_RATIONALE;
+import static org.briarproject.briar.android.util.Permission.UNKNOWN;
 import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
 import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
 import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
 import static org.briarproject.briar.android.util.UiUtils.showRationale;
+import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
 
 class AddNearbyContactPermissionManager {
 
-	private Permission cameraPermission = Permission.UNKNOWN;
-	private Permission locationPermission = Permission.UNKNOWN;
+	private Permission cameraPermission = UNKNOWN;
+	private Permission locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED;
+	private Permission bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN;
 
 	private final FragmentActivity ctx;
 	private final Consumer<String[]> requestPermissions;
@@ -39,23 +50,32 @@ class AddNearbyContactPermissionManager {
 	}
 
 	void resetPermissions() {
-		cameraPermission = Permission.UNKNOWN;
-		locationPermission = Permission.UNKNOWN;
+		cameraPermission = UNKNOWN;
+		locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED;
+		bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN;
 	}
 
 	static boolean areEssentialPermissionsGranted(Context ctx,
 			boolean isBluetoothSupported) {
 		int ok = PERMISSION_GRANTED;
-		return checkSelfPermission(ctx, CAMERA) == ok &&
-				(SDK_INT < 23 ||
-						checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok ||
-						!isBluetoothSupported);
+		boolean bluetoothOk;
+		if (!isBluetoothSupported || SDK_INT < 23) {
+			bluetoothOk = true;
+		} else if (SDK_INT < 31) {
+			bluetoothOk = checkSelfPermission(ctx, ACCESS_FINE_LOCATION) == ok;
+		} else {
+			bluetoothOk = hasBtConnectPermission(ctx) &&
+					hasBtScanPermission(ctx) &&
+					checkSelfPermission(ctx, BLUETOOTH_ADVERTISE) == ok;
+		}
+		return bluetoothOk && checkSelfPermission(ctx, CAMERA) == ok;
 	}
 
 	private boolean areEssentialPermissionsGranted() {
-		return cameraPermission == Permission.GRANTED &&
-				(SDK_INT < 23 || locationPermission == Permission.GRANTED ||
-						!isBluetoothSupported);
+		boolean bluetoothGranted = locationPermission == GRANTED &&
+				bluetoothPermissions == GRANTED;
+		return cameraPermission == GRANTED &&
+				(SDK_INT < 23 || !isBluetoothSupported || bluetoothGranted);
 	}
 
 	boolean checkPermissions() {
@@ -63,31 +83,40 @@ class AddNearbyContactPermissionManager {
 		if (locationEnabled && areEssentialPermissionsGranted()) return true;
 		// If an essential permission has been permanently denied, ask the
 		// user to change the setting
-		if (cameraPermission == Permission.PERMANENTLY_DENIED) {
+		if (cameraPermission == PERMANENTLY_DENIED) {
 			showDenialDialog(ctx, R.string.permission_camera_title,
 					R.string.permission_camera_denied_body);
 			return false;
 		}
-		if (isBluetoothSupported &&
-				locationPermission == Permission.PERMANENTLY_DENIED) {
+		if (isBluetoothSupported && locationPermission == PERMANENTLY_DENIED) {
 			showDenialDialog(ctx, R.string.permission_location_title,
 					R.string.permission_location_denied_body);
 			return false;
 		}
+		if (isBluetoothSupported &&
+				bluetoothPermissions == PERMANENTLY_DENIED) {
+			showDenialDialog(ctx, R.string.permission_bluetooth_title,
+					R.string.permission_bluetooth_denied_body);
+			return false;
+		}
 		// Should we show the rationale for one or both permissions?
-		if (cameraPermission == Permission.SHOW_RATIONALE &&
-				locationPermission == Permission.SHOW_RATIONALE) {
+		if (cameraPermission == SHOW_RATIONALE &&
+				locationPermission == SHOW_RATIONALE) {
 			showRationale(ctx, R.string.permission_camera_location_title,
 					R.string.permission_camera_location_request_body,
 					this::requestPermissions);
-		} else if (cameraPermission == Permission.SHOW_RATIONALE) {
+		} else if (cameraPermission == SHOW_RATIONALE) {
 			showRationale(ctx, R.string.permission_camera_title,
 					R.string.permission_camera_request_body,
 					this::requestPermissions);
-		} else if (locationPermission == Permission.SHOW_RATIONALE) {
+		} else if (locationPermission == SHOW_RATIONALE) {
 			showRationale(ctx, R.string.permission_location_title,
 					R.string.permission_location_request_body,
 					this::requestPermissions);
+		} else if (bluetoothPermissions == SHOW_RATIONALE) {
+			showRationale(ctx, R.string.permission_bluetooth_title,
+					R.string.permission_bluetooth_body,
+					this::requestPermissions);
 		} else if (locationEnabled) {
 			requestPermissions();
 		} else {
@@ -99,7 +128,12 @@ class AddNearbyContactPermissionManager {
 	private void requestPermissions() {
 		String[] permissions;
 		if (isBluetoothSupported) {
-			permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION};
+			if (SDK_INT < 31) {
+				permissions = new String[] {CAMERA, ACCESS_FINE_LOCATION};
+			} else {
+				permissions = new String[] {CAMERA, BLUETOOTH_ADVERTISE,
+						BLUETOOTH_CONNECT, BLUETOOTH_SCAN};
+			}
 		} else {
 			permissions = new String[] {CAMERA};
 		}
@@ -108,19 +142,29 @@ class AddNearbyContactPermissionManager {
 
 	void onRequestPermissionResult(Map<String, Boolean> result) {
 		if (gotPermission(CAMERA, result)) {
-			cameraPermission = Permission.GRANTED;
+			cameraPermission = GRANTED;
 		} else if (shouldShowRationale(CAMERA)) {
-			cameraPermission = Permission.SHOW_RATIONALE;
+			cameraPermission = SHOW_RATIONALE;
 		} else {
-			cameraPermission = Permission.PERMANENTLY_DENIED;
+			cameraPermission = PERMANENTLY_DENIED;
 		}
 		if (isBluetoothSupported) {
-			if (gotPermission(ACCESS_FINE_LOCATION, result)) {
-				locationPermission = Permission.GRANTED;
-			} else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
-				locationPermission = Permission.SHOW_RATIONALE;
+			if (SDK_INT < 31) {
+				if (gotPermission(ACCESS_FINE_LOCATION, result)) {
+					locationPermission = GRANTED;
+				} else if (shouldShowRationale(ACCESS_FINE_LOCATION)) {
+					locationPermission = SHOW_RATIONALE;
+				} else {
+					locationPermission = PERMANENTLY_DENIED;
+				}
 			} else {
-				locationPermission = Permission.PERMANENTLY_DENIED;
+				if (wasGrantedBluetoothPermissions(result)) {
+					bluetoothPermissions = GRANTED;
+				} else if (shouldShowRationale(BLUETOOTH_CONNECT)) {
+					bluetoothPermissions = SHOW_RATIONALE;
+				} else {
+					bluetoothPermissions = PERMANENTLY_DENIED;
+				}
 			}
 		}
 	}
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java
index f5c543145c03034c0c38328dd6a479974ae48b68..4920e6da6a7d9e8f71c2fb1c39bdc90ed6194c6b 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/nearby/AddNearbyContactViewModel.java
@@ -1,5 +1,6 @@
 package org.briarproject.briar.android.contact.add.nearby;
 
+import android.annotation.SuppressLint;
 import android.app.Application;
 import android.bluetooth.BluetoothAdapter;
 import android.content.BroadcastReceiver;
@@ -250,6 +251,7 @@ class AddNearbyContactViewModel extends AndroidViewModel
 	}
 
 	@UiThread
+	@SuppressLint("MissingPermission") // we check permissions before
 	private boolean isBluetoothReady() {
 		if (bt == null || bluetoothPlugin == null) {
 			// Continue without Bluetooth
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java
index 7a8ac1d9ae0bb15099ad1b180d5fd077317f4d02..fc80179bc4b62ebc66750d8ac40237709a5f3a91 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothConditionManager.java
@@ -5,58 +5,101 @@ import android.content.Context;
 
 import org.briarproject.briar.R;
 import org.briarproject.briar.android.util.Permission;
+import org.briarproject.briar.android.util.UiUtils;
+
+import java.util.Map;
 
 import androidx.activity.result.ActivityResultLauncher;
 import androidx.annotation.Nullable;
 import androidx.annotation.UiThread;
 import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.FragmentActivity;
 
 import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Build.VERSION.SDK_INT;
 import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
+import static androidx.core.content.ContextCompat.checkSelfPermission;
+import static org.briarproject.briar.android.util.Permission.GRANTED;
+import static org.briarproject.briar.android.util.Permission.PERMANENTLY_DENIED;
+import static org.briarproject.briar.android.util.Permission.SHOW_RATIONALE;
+import static org.briarproject.briar.android.util.Permission.UNKNOWN;
 import static org.briarproject.briar.android.util.UiUtils.getGoToSettingsListener;
 import static org.briarproject.briar.android.util.UiUtils.isLocationEnabled;
+import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions;
 import static org.briarproject.briar.android.util.UiUtils.showLocationDialog;
+import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
 
 class BluetoothConditionManager {
 
-	private Permission locationPermission = Permission.UNKNOWN;
+	private Permission locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED;
+	private Permission bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN;
 
 	/**
 	 * Call this when the using activity or fragment starts,
 	 * because permissions might have changed while it was stopped.
 	 */
 	void reset() {
-		locationPermission = Permission.UNKNOWN;
+		locationPermission = SDK_INT < 31 ? UNKNOWN : GRANTED;
+		bluetoothPermissions = SDK_INT < 31 ? GRANTED : UNKNOWN;
+	}
+
+	@UiThread
+	void requestPermissions(ActivityResultLauncher<String[]> launcher) {
+		if (SDK_INT < 31) {
+			launcher.launch(new String[] {ACCESS_FINE_LOCATION});
+		} else {
+			requestBluetoothPermissions(launcher);
+		}
 	}
 
 	@UiThread
 	void onLocationPermissionResult(Activity activity,
-			@Nullable Boolean result) {
-		if (result != null && result) {
-			locationPermission = Permission.GRANTED;
-		} else if (shouldShowRequestPermissionRationale(activity,
-				ACCESS_FINE_LOCATION)) {
-			locationPermission = Permission.SHOW_RATIONALE;
+			@Nullable Map<String, Boolean> result) {
+		if (SDK_INT < 31) {
+			if (gotPermission(activity, result)) {
+				locationPermission = GRANTED;
+			} else if (shouldShowRequestPermissionRationale(activity,
+					ACCESS_FINE_LOCATION)) {
+				locationPermission = SHOW_RATIONALE;
+			} else {
+				locationPermission = PERMANENTLY_DENIED;
+			}
 		} else {
-			locationPermission = Permission.PERMANENTLY_DENIED;
+			if (wasGrantedBluetoothPermissions(result)) {
+				bluetoothPermissions = GRANTED;
+			} else if (shouldShowRequestPermissionRationale(activity,
+					BLUETOOTH_CONNECT)) {
+				bluetoothPermissions = SHOW_RATIONALE;
+			} else {
+				bluetoothPermissions = PERMANENTLY_DENIED;
+			}
 		}
 	}
 
-	boolean areRequirementsFulfilled(Context ctx,
-			ActivityResultLauncher<String> permissionRequest,
+	boolean areRequirementsFulfilled(FragmentActivity ctx,
+			ActivityResultLauncher<String[]> permissionRequest,
 			Runnable onLocationDenied) {
 		boolean permissionGranted =
-				SDK_INT < 23 || locationPermission == Permission.GRANTED;
+				(SDK_INT < 23 || locationPermission == GRANTED) &&
+						bluetoothPermissions == GRANTED;
 		boolean locationEnabled = isLocationEnabled(ctx);
 		if (permissionGranted && locationEnabled) return true;
 
-		if (locationPermission == Permission.PERMANENTLY_DENIED) {
+		if (locationPermission == PERMANENTLY_DENIED) {
 			showDenialDialog(ctx, onLocationDenied);
-		} else if (locationPermission == Permission.SHOW_RATIONALE) {
+		} else if (locationPermission == SHOW_RATIONALE) {
 			showRationale(ctx, permissionRequest);
 		} else if (!locationEnabled) {
 			showLocationDialog(ctx);
+		} else if (bluetoothPermissions == PERMANENTLY_DENIED) {
+			UiUtils.showDenialDialog(ctx, R.string.permission_bluetooth_title,
+					R.string.permission_bluetooth_denied_body);
+		} else if (bluetoothPermissions == SHOW_RATIONALE && SDK_INT >= 31) {
+			UiUtils.showRationale(ctx, R.string.permission_bluetooth_title,
+					R.string.permission_bluetooth_body, () ->
+							requestBluetoothPermissions(permissionRequest));
 		}
 		return false;
 	}
@@ -72,13 +115,27 @@ class BluetoothConditionManager {
 	}
 
 	private void showRationale(Context ctx,
-			ActivityResultLauncher<String> permissionRequest) {
+			ActivityResultLauncher<String[]> permissionRequest) {
 		new AlertDialog.Builder(ctx, R.style.BriarDialogTheme)
 				.setTitle(R.string.permission_location_title)
 				.setMessage(R.string.permission_location_request_body)
 				.setPositiveButton(R.string.ok, (dialog, which) ->
-						permissionRequest.launch(ACCESS_FINE_LOCATION))
+						permissionRequest.launch(
+								new String[] {ACCESS_FINE_LOCATION}))
 				.show();
 	}
 
+	private boolean gotPermission(Context ctx,
+			@Nullable Map<String, Boolean> result) {
+		Boolean permissionResult =
+				result == null ? null : result.get(ACCESS_FINE_LOCATION);
+		return permissionResult == null ? isLocationPermissionGranted(ctx) :
+				permissionResult;
+	}
+
+	private boolean isLocationPermissionGranted(Context ctx) {
+		return checkSelfPermission(ctx, ACCESS_FINE_LOCATION) ==
+				PERMISSION_GRANTED;
+	}
+
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java
index ed774bcd2d1d2be3a9b4b6aa60b52b098f91ef5c..2e2413f5182a68466fc4df76b5992c3e9db2a25c 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/connect/BluetoothIntroFragment.java
@@ -1,6 +1,5 @@
 package org.briarproject.briar.android.contact.connect;
 
-import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -14,15 +13,17 @@ import org.briarproject.briar.android.util.ActivityLaunchers.RequestBluetoothDis
 import org.briarproject.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.nullsafety.ParametersNotNullByDefault;
 
+import java.util.Map;
+
 import javax.inject.Inject;
 
 import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
 import androidx.lifecycle.ViewModelProvider;
 
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
 import static android.widget.Toast.LENGTH_LONG;
 import static org.briarproject.briar.android.AppModule.getAndroidComponent;
 
@@ -42,8 +43,8 @@ public class BluetoothIntroFragment extends Fragment {
 	private final ActivityResultLauncher<Integer> bluetoothDiscoverableRequest =
 			registerForActivityResult(new RequestBluetoothDiscoverable(),
 					this::onBluetoothDiscoverable);
-	private final ActivityResultLauncher<String> permissionRequest =
-			registerForActivityResult(new RequestPermission(),
+	private final ActivityResultLauncher<String[]> permissionRequest =
+			registerForActivityResult(new RequestMultiplePermissions(),
 					this::onPermissionRequestResult);
 
 	@Override
@@ -80,12 +81,13 @@ public class BluetoothIntroFragment extends Fragment {
 			// if the permission is already granted.
 			// So we can use the request as a generic entry point
 			// to the whole flow.
-			permissionRequest.launch(ACCESS_FINE_LOCATION);
+			conditionManager.requestPermissions(permissionRequest);
 		}
 	}
 
-	private void onPermissionRequestResult(@Nullable Boolean result) {
-		Activity a = requireActivity();
+	private void onPermissionRequestResult(
+			@Nullable Map<String, Boolean> result) {
+		FragmentActivity a = requireActivity();
 		// update permission result in BluetoothConnecter
 		conditionManager.onLocationPermissionResult(a, result);
 		// what to do when the user denies granting the location permission
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java
index 81de79aa4d9ca9f4eb245c833f17206424c7d3f4..283b9f0eba2d09998931314295bd706ca4143532 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/navdrawer/TransportsActivity.java
@@ -26,18 +26,24 @@ import org.briarproject.nullsafety.ParametersNotNullByDefault;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 import javax.inject.Inject;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
 import androidx.annotation.ColorRes;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
 import androidx.annotation.StringRes;
 import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.widget.SwitchCompat;
 import androidx.core.content.ContextCompat;
 import androidx.lifecycle.ViewModelProvider;
 
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.os.Build.VERSION.SDK_INT;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
@@ -47,7 +53,13 @@ import static org.briarproject.bramble.api.plugin.Plugin.State.STARTING_STOPPING
 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.util.AndroidUtils.hasBtConnectPermission;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
+import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions;
+import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
 import static org.briarproject.briar.android.util.UiUtils.showOnboardingDialog;
+import static org.briarproject.briar.android.util.UiUtils.showRationale;
+import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
@@ -61,6 +73,11 @@ public class TransportsActivity extends BriarActivity {
 	private PluginViewModel viewModel;
 	private BaseAdapter transportsAdapter;
 
+	@RequiresApi(31)
+	private final ActivityResultLauncher<String[]> requestPermissionLauncher =
+			registerForActivityResult(new RequestMultiplePermissions(),
+					this::handleBtPermissionResult);
+
 	@Override
 	public void injectActivity(ActivityComponent component) {
 		component.inject(this);
@@ -149,8 +166,7 @@ public class TransportsActivity extends BriarActivity {
 						view.findViewById(R.id.switchCompat);
 				switchCompat.setText(getString(t.switchLabel));
 				switchCompat.setOnClickListener(v ->
-						viewModel.enableTransport(t.id,
-								switchCompat.isChecked()));
+						onClicked(t.id, switchCompat.isChecked()));
 				switchCompat.setChecked(t.isSwitchChecked);
 
 				TextView summary = view.findViewById(R.id.summary);
@@ -203,6 +219,21 @@ public class TransportsActivity extends BriarActivity {
 		});
 	}
 
+	private void onClicked(TransportId transportId, boolean enable) {
+		if (enable && SDK_INT >= 31 &&
+				(!hasBtConnectPermission(this) || !hasBtScanPermission(this))) {
+			if (shouldShowRequestPermissionRationale(BLUETOOTH_CONNECT)) {
+				showRationale(this, R.string.permission_bluetooth_title,
+						R.string.permission_bluetooth_body,
+						this::requestBtPermissions);
+			} else {
+				requestBtPermissions();
+			}
+		} else {
+			viewModel.enableTransport(transportId, enable);
+		}
+	}
+
 	private String getBulletString(@StringRes int resId) {
 		return "\u2022 " + getString(resId);
 	}
@@ -316,6 +347,23 @@ public class TransportsActivity extends BriarActivity {
 		return transport;
 	}
 
+	@RequiresApi(31)
+	private void requestBtPermissions() {
+		requestBluetoothPermissions(requestPermissionLauncher);
+	}
+
+	@RequiresApi(31)
+	private void handleBtPermissionResult(Map<String, Boolean> grantedMap) {
+		if (wasGrantedBluetoothPermissions(grantedMap)) {
+			viewModel.enableTransport(BluetoothConstants.ID, true);
+		} else {
+			transportsAdapter.notifyDataSetChanged();
+			showDenialDialog(this,
+					R.string.permission_bluetooth_title,
+					R.string.permission_bluetooth_denied_body);
+		}
+	}
+
 	private static class Transport {
 
 		private final TransportId id;
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java
index ef9e6ad941ff986cf23674936eab315b7209d142..66f90e48cb20adb6f9ff75e725db5f5e661d2695 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/reporting/BriarReportCollector.java
@@ -58,6 +58,8 @@ import static java.util.Locale.US;
 import static java.util.Objects.requireNonNull;
 import static java.util.TimeZone.getTimeZone;
 import static org.briarproject.bramble.util.AndroidUtils.getBluetoothAddressAndMethod;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
 import static org.briarproject.bramble.util.PrivacyUtils.scrubInetAddress;
 import static org.briarproject.bramble.util.PrivacyUtils.scrubMacAddress;
 import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
@@ -273,12 +275,13 @@ class BriarReportCollector {
 
 			// Is Bluetooth enabled?
 			@SuppressLint("HardwareIds")
-			boolean btEnabled = bt.isEnabled()
+			boolean btEnabled = hasBtConnectPermission(ctx) && bt.isEnabled()
 					&& !isNullOrEmpty(bt.getAddress());
 			connectivityInfo.add("BluetoothEnabled", btEnabled);
 
 			// Is Bluetooth connectable?
-			int scanMode = bt.getScanMode();
+			@SuppressLint("MissingPermission")
+			int scanMode = hasBtScanPermission(ctx) ? bt.getScanMode() : -1;
 			boolean btConnectable = scanMode == SCAN_MODE_CONNECTABLE ||
 					scanMode == SCAN_MODE_CONNECTABLE_DISCOVERABLE;
 			connectivityInfo.add("BluetoothConnectable", btConnectable);
@@ -298,11 +301,14 @@ class BriarReportCollector {
 						btLeAdvertise);
 			}
 
-			Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt);
-			String address = p.getFirst();
-			String method = p.getSecond();
-			connectivityInfo.add("BluetoothAddress", scrubMacAddress(address));
-			connectivityInfo.add("BluetoothAddressMethod", method);
+			if (hasBtConnectPermission(ctx)) {
+				Pair<String, String> p = getBluetoothAddressAndMethod(ctx, bt);
+				String address = p.getFirst();
+				String method = p.getSecond();
+				connectivityInfo.add("BluetoothAddress",
+						scrubMacAddress(address));
+				connectivityInfo.add("BluetoothAddressMethod", method);
+			}
 		}
 		return new ReportItem("Connectivity", R.string.dev_report_connectivity,
 				connectivityInfo);
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java
index afb594ab59384eb70cca94c5b62127527abcf0dc..465de4441a36eff76d40f76d52351c1fbc7138ea 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/settings/ConnectionsFragment.java
@@ -8,18 +8,32 @@ import org.briarproject.briar.R;
 import org.briarproject.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.nullsafety.ParametersNotNullByDefault;
 
+import java.util.Map;
+
 import javax.inject.Inject;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.fragment.app.FragmentActivity;
 import androidx.lifecycle.LifecycleOwner;
 import androidx.lifecycle.ViewModelProvider;
 import androidx.preference.ListPreference;
 import androidx.preference.PreferenceFragmentCompat;
 import androidx.preference.SwitchPreferenceCompat;
 
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.os.Build.VERSION.SDK_INT;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtConnectPermission;
+import static org.briarproject.bramble.util.AndroidUtils.hasBtScanPermission;
 import static org.briarproject.briar.android.AppModule.getAndroidComponent;
 import static org.briarproject.briar.android.settings.SettingsActivity.enableAndPersist;
+import static org.briarproject.briar.android.util.UiUtils.requestBluetoothPermissions;
+import static org.briarproject.briar.android.util.UiUtils.showDenialDialog;
+import static org.briarproject.briar.android.util.UiUtils.showRationale;
+import static org.briarproject.briar.android.util.UiUtils.wasGrantedBluetoothPermissions;
 
 @MethodsNotNullByDefault
 @ParametersNotNullByDefault
@@ -47,6 +61,11 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
 	private SwitchPreferenceCompat torMobile;
 	private SwitchPreferenceCompat torOnlyWhenCharging;
 
+	@RequiresApi(31)
+	private final ActivityResultLauncher<String[]> requestPermissionLauncher =
+			registerForActivityResult(new RequestMultiplePermissions(),
+					this::handleBtPermissionResult);
+
 	@Override
 	public void onAttach(@NonNull Context context) {
 		super.onAttach(context);
@@ -69,6 +88,25 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
 
 		torNetwork.setSummaryProvider(viewModel.torSummaryProvider);
 
+		if (SDK_INT >= 31) {
+			enableBluetooth.setOnPreferenceChangeListener((p, value) -> {
+				FragmentActivity ctx = requireActivity();
+				if (hasBtConnectPermission(ctx) && hasBtScanPermission(ctx)) {
+					return true;
+				} else if (shouldShowRequestPermissionRationale(
+						BLUETOOTH_CONNECT)) {
+					showRationale(ctx, R.string.permission_bluetooth_title,
+							R.string.permission_bluetooth_body,
+							this::requestBtPermissions);
+					// we don't update the preference directly,
+					// but do it via the launcher, if we got the permissions
+					return false;
+				} else {
+					requestBtPermissions();
+					return false;
+				}
+			});
+		}
 		enableBluetooth.setPreferenceDataStore(connectionsManager.btStore);
 		enableWifi.setPreferenceDataStore(connectionsManager.wifiStore);
 		enableTor.setPreferenceDataStore(connectionsManager.torStore);
@@ -115,4 +153,19 @@ public class ConnectionsFragment extends PreferenceFragmentCompat {
 		requireActivity().setTitle(R.string.network_settings_title);
 	}
 
+	@RequiresApi(31)
+	private void requestBtPermissions() {
+		requestBluetoothPermissions(requestPermissionLauncher);
+	}
+
+	@RequiresApi(31)
+	private void handleBtPermissionResult(Map<String, Boolean> grantedMap) {
+		if (wasGrantedBluetoothPermissions(grantedMap)) {
+			enableBluetooth.setChecked(true);
+		} else {
+			showDenialDialog(requireActivity(),
+					R.string.permission_bluetooth_title,
+					R.string.permission_bluetooth_denied_body);
+		}
+	}
 }
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 baa8939640bce589940c537242d0404ee90fafd5..b97ee36e3ac1f7f73a47cafc302154b8f546154b 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
@@ -44,6 +44,7 @@ import org.briarproject.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.nullsafety.ParametersNotNullByDefault;
 
 import java.util.Locale;
+import java.util.Map;
 import java.util.logging.Logger;
 
 import androidx.activity.result.ActivityResultLauncher;
@@ -70,6 +71,9 @@ import androidx.lifecycle.LiveData;
 import androidx.lifecycle.Observer;
 import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
 
+import static android.Manifest.permission.BLUETOOTH_ADVERTISE;
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_SCAN;
 import static android.content.Context.KEYGUARD_SERVICE;
 import static android.content.Intent.CATEGORY_DEFAULT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
@@ -105,6 +109,7 @@ import static androidx.core.content.ContextCompat.getColor;
 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.lang.Boolean.TRUE;
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.logging.Level.WARNING;
@@ -346,10 +351,10 @@ public class UiUtils {
 
 	/**
 	 * @return true if location is enabled,
-	 * or it isn't required due to this being a SDK < 28 device.
+	 * or it isn't required due to this being a device with SDK < 28 or >= 31.
 	 */
 	public static boolean isLocationEnabled(Context ctx) {
-		if (SDK_INT >= 28) {
+		if (SDK_INT >= 28 && SDK_INT < 31) {
 			LocationManager lm = ctx.getSystemService(LocationManager.class);
 			return lm.isLocationEnabled();
 		} else {
@@ -625,4 +630,21 @@ public class UiUtils {
 		}
 		Toast.makeText(ctx, R.string.error_start_activity, LENGTH_LONG).show();
 	}
+
+	@RequiresApi(31)
+	public static void requestBluetoothPermissions(
+			ActivityResultLauncher<String[]> launcher) {
+		String[] perms = new String[] {BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT,
+				BLUETOOTH_SCAN};
+		launcher.launch(perms);
+	}
+
+	@RequiresApi(31)
+	public static boolean wasGrantedBluetoothPermissions(
+			@Nullable Map<String, Boolean> grantedMap) {
+		return grantedMap != null &&
+				TRUE.equals(grantedMap.get(BLUETOOTH_ADVERTISE)) &&
+				TRUE.equals(grantedMap.get(BLUETOOTH_CONNECT)) &&
+				TRUE.equals(grantedMap.get(BLUETOOTH_SCAN));
+	}
 }
diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml
index 3bf3edf4952666a947b2a507b0be3b0b249052a1..0d310498676683e4ee96537ba55f2990322c3626 100644
--- a/briar-android/src/main/res/values/strings.xml
+++ b/briar-android/src/main/res/values/strings.xml
@@ -782,6 +782,10 @@
 	<string name="permission_location_setting_title">Location setting</string>
 	<string name="permission_location_setting_body">Your device\'s location setting must be turned on to find other devices via Bluetooth. Please enable location to continue. You can disable it again afterwards.</string>
 	<string name="permission_location_setting_button">Enable location</string>
+	<string name="permission_bluetooth_title">Nearby devices permission</string>
+	<string name="permission_bluetooth_body">To use Bluetooth communication, Briar needs permission to find and connect to nearby devices.</string>
+	<string name="permission_bluetooth_denied_body">You have denied access to nearby devices, but Briar needs this permission to use Bluetooth.\n\nPlease consider granting access.</string>
+
 	<string name="qr_code">QR code</string>
 	<string name="show_qr_code_fullscreen">Show QR code fullscreen</string>