diff --git a/app/src/main/java/org/briarproject/hotspot/AbstractConditionManager.java b/app/src/main/java/org/briarproject/hotspot/AbstractConditionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec3e1f43fb6490fd4256f4f2514b66cbaa40e25b
--- /dev/null
+++ b/app/src/main/java/org/briarproject/hotspot/AbstractConditionManager.java
@@ -0,0 +1,53 @@
+package org.briarproject.hotspot;
+
+import android.net.wifi.WifiManager;
+
+import androidx.core.util.Consumer;
+import androidx.fragment.app.FragmentActivity;
+
+import static android.content.Context.WIFI_SERVICE;
+
+/**
+ * Abstract base class for the ConditionManagers that ensure that the conditions
+ * to open a hotspot are fulfilled. There are different extensions of this for
+ * API levels lower than 29 and 29+.
+ */
+abstract class AbstractConditionManager {
+
+	enum Permission {
+		UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
+	}
+
+	protected final Consumer<Boolean> permissionUpdateCallback;
+	protected FragmentActivity ctx;
+	protected WifiManager wifiManager;
+
+	AbstractConditionManager(Consumer<Boolean> permissionUpdateCallback) {
+		this.permissionUpdateCallback = permissionUpdateCallback;
+	}
+
+	/**
+	 * Pass a FragmentActivity context here during `onCreateView()`.
+	 */
+	void init(FragmentActivity ctx) {
+		this.ctx = ctx;
+		this.wifiManager = (WifiManager) ctx.getApplicationContext()
+				.getSystemService(WIFI_SERVICE);
+	}
+
+	/**
+	 * Call this during onStart() in the fragment where the ConditionManager
+	 * is used.
+	 */
+	abstract void onStart();
+
+	/**
+	 * Check if all required conditions are met such that the hotspot can be
+	 * started. If any precondition is not met yet, bring up relevant dialogs
+	 * asking the user to grant relevant permissions or take relevant actions.
+	 *
+	 * @return true if conditions are fulfilled and flow can continue.
+	 */
+	abstract boolean checkAndRequestConditions();
+
+}
diff --git a/app/src/main/java/org/briarproject/hotspot/ConditionManager.java b/app/src/main/java/org/briarproject/hotspot/ConditionManager.java
index 971fe5dbff92c7fd0c7d43e2d2d9ecc2007a1390..05d8988da62d93031aad9c9ce7e84920835f31c9 100644
--- a/app/src/main/java/org/briarproject/hotspot/ConditionManager.java
+++ b/app/src/main/java/org/briarproject/hotspot/ConditionManager.java
@@ -1,166 +1,82 @@
 package org.briarproject.hotspot;
 
-import android.content.DialogInterface.OnClickListener;
 import android.content.Intent;
-import android.net.wifi.WifiManager;
 import android.provider.Settings;
 
-import androidx.activity.result.ActivityResult;
+import java.util.logging.Logger;
+
+import androidx.activity.result.ActivityResultCaller;
 import androidx.activity.result.ActivityResultLauncher;
-import androidx.annotation.StringRes;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.FragmentActivity;
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
+import androidx.core.util.Consumer;
 
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
-import static android.content.Context.WIFI_SERVICE;
-import static android.os.Build.VERSION.SDK_INT;
-import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
-import static org.briarproject.hotspot.UiUtils.getGoToSettingsListener;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.hotspot.UiUtils.showRationale;
 
 /**
- * This class ensures that the conditions to open a hotspot are fulfilled.
- * <p>
- * Be sure to call {@link #onRequestPermissionResult(Boolean)} and
- * {@link #onRequestWifiEnabledResult()} when you get the
- * {@link ActivityResult}.
+ * This class ensures that the conditions to open a hotspot are fulfilled on
+ * API levels < 29.
  * <p>
  * As soon as {@link #checkAndRequestConditions()} returns true,
  * all conditions are fulfilled.
  */
-class ConditionManager {
-
-	private enum Permission {
-		UNKNOWN, GRANTED, SHOW_RATIONALE, PERMANENTLY_DENIED
-	}
+class ConditionManager extends AbstractConditionManager {
 
-	private Permission locationPermission = Permission.UNKNOWN;
-	private Permission wifiSetting = Permission.SHOW_RATIONALE;
+	private static final Logger LOG =
+			getLogger(ConditionManager.class.getName());
 
-	private final FragmentActivity ctx;
-	private final WifiManager wifiManager;
-	private final ActivityResultLauncher<String> locationRequest;
 	private final ActivityResultLauncher<Intent> wifiRequest;
 
-	ConditionManager(FragmentActivity ctx,
-			ActivityResultLauncher<String> locationRequest,
-			ActivityResultLauncher<Intent> wifiRequest) {
-		this.ctx = ctx;
-		this.wifiManager = (WifiManager) ctx.getApplicationContext()
-				.getSystemService(WIFI_SERVICE);
-		this.locationRequest = locationRequest;
-		this.wifiRequest = wifiRequest;
+	ConditionManager(ActivityResultCaller arc,
+			Consumer<Boolean> permissionUpdateCallback) {
+		super(permissionUpdateCallback);
+		wifiRequest = arc.registerForActivityResult(
+				new StartActivityForResult(),
+				result -> permissionUpdateCallback
+						.accept(wifiManager.isWifiEnabled()));
 	}
 
-	/**
-	 * Call this to reset state when UI starts,
-	 * because state might have changed.
-	 */
-	void resetPermissions() {
-		locationPermission = Permission.UNKNOWN;
-		wifiSetting = Permission.SHOW_RATIONALE;
+	@Override
+	void onStart() {
+		// nothing to do here
 	}
 
-	/**
-	 * This makes a request for location permission.
-	 * If {@link #checkAndRequestConditions()} returns true, you can continue.
-	 */
-	void startConditionChecks() {
-		locationRequest.launch(ACCESS_FINE_LOCATION);
+	private boolean areEssentialPermissionsGranted() {
+		if (LOG.isLoggable(INFO)) {
+			LOG.info(String.format("areEssentialPermissionsGranted(): " +
+							"wifiManager.isWifiEnabled()? %b",
+					wifiManager.isWifiEnabled()));
+		}
+		return wifiManager.isWifiEnabled();
 	}
 
-	/**
-	 * @return true if conditions are fulfilled and flow can continue.
-	 */
+	@Override
 	boolean checkAndRequestConditions() {
 		if (areEssentialPermissionsGranted()) return true;
 
-		// If an essential permission has been permanently denied, ask the
-		// user to change the setting
-		if (locationPermission == Permission.PERMANENTLY_DENIED) {
-			showDenialDialog(R.string.permission_location_title,
-					R.string.permission_hotspot_location_denied_body,
-					getGoToSettingsListener(ctx));
-			return false;
-		}
-		if (wifiSetting == Permission.PERMANENTLY_DENIED) {
-			showDenialDialog(R.string.wifi_settings_title,
-					R.string.wifi_settings_request_denied_body,
-					(d, w) -> requestEnableWiFi());
-			return false;
-		}
+		if (!wifiManager.isWifiEnabled()) {
+			// Try enabling the Wifi and return true if that seems to have been
+			// successful, i.e. "Wifi is either already in the requested state, or
+			// in progress toward the requested state".
+			if (wifiManager.setWifiEnabled(true)) {
+				LOG.info("Enabled wifi");
+				return true;
+			}
 
-		// Should we show the rationale for location permission or Wi-Fi?
-		if (locationPermission == Permission.SHOW_RATIONALE) {
-			showRationale(R.string.permission_location_title,
-					R.string.permission_hotspot_location_request_body,
-					this::requestPermissions);
-		} else if (wifiSetting == Permission.SHOW_RATIONALE) {
-			showRationale(R.string.wifi_settings_title,
+			// Wifi is not enabled and we can't seem to enable it, so ask the user
+			// to enable it for us.
+			showRationale(ctx, R.string.wifi_settings_title,
 					R.string.wifi_settings_request_enable_body,
-					this::requestEnableWiFi);
-		}
-		return false;
-	}
-
-	void onRequestPermissionResult(Boolean granted) {
-		if (granted != null && granted) {
-			locationPermission = Permission.GRANTED;
-		} else if (shouldShowRequestPermissionRationale(ctx,
-				ACCESS_FINE_LOCATION)) {
-			locationPermission = Permission.SHOW_RATIONALE;
-		} else {
-			locationPermission = Permission.PERMANENTLY_DENIED;
+					this::requestEnableWiFi,
+					() -> permissionUpdateCallback.accept(false));
 		}
-	}
-
-	void onRequestWifiEnabledResult() {
-		wifiSetting = wifiManager.isWifiEnabled() ? Permission.GRANTED :
-				Permission.PERMANENTLY_DENIED;
-	}
 
-	private boolean areEssentialPermissionsGranted() {
-		if (SDK_INT < 29) {
-			if (!wifiManager.isWifiEnabled()) {
-				//noinspection deprecation
-				return wifiManager.setWifiEnabled(true);
-			}
-			return true;
-		} else {
-			return locationPermission == Permission.GRANTED
-					&& wifiManager.isWifiEnabled();
-		}
-	}
-
-	private void showDenialDialog(@StringRes int title, @StringRes int body,
-			OnClickListener onOkClicked) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
-		builder.setTitle(title);
-		builder.setMessage(body);
-		builder.setPositiveButton(R.string.ok, onOkClicked);
-		builder.setNegativeButton(R.string.cancel,
-				(dialog, which) -> ctx.supportFinishAfterTransition());
-		builder.show();
-	}
-
-	private void showRationale(@StringRes int title, @StringRes int body,
-			Runnable onContinueClicked) {
-		AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
-		builder.setTitle(title);
-		builder.setMessage(body);
-		builder.setNeutralButton(R.string.continue_button,
-				(dialog, which) -> onContinueClicked.run());
-		builder.show();
-	}
-
-	private void requestPermissions() {
-		locationRequest.launch(ACCESS_FINE_LOCATION);
+		return false;
 	}
 
 	private void requestEnableWiFi() {
-		Intent i = SDK_INT < 29 ?
-				new Intent(Settings.ACTION_WIFI_SETTINGS) :
-				new Intent(Settings.Panel.ACTION_WIFI);
-		wifiRequest.launch(i);
+		wifiRequest.launch(new Intent(Settings.ACTION_WIFI_SETTINGS));
 	}
 
 }
diff --git a/app/src/main/java/org/briarproject/hotspot/ConditionManager29.java b/app/src/main/java/org/briarproject/hotspot/ConditionManager29.java
new file mode 100644
index 0000000000000000000000000000000000000000..78888bc5f22f1be77ba9612736ee8a7f537c738c
--- /dev/null
+++ b/app/src/main/java/org/briarproject/hotspot/ConditionManager29.java
@@ -0,0 +1,136 @@
+package org.briarproject.hotspot;
+
+import android.content.Intent;
+import android.provider.Settings;
+
+import java.util.logging.Logger;
+
+import androidx.activity.result.ActivityResultCaller;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.util.Consumer;
+
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale;
+import static java.lang.Boolean.TRUE;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Logger.getLogger;
+import static org.briarproject.hotspot.UiUtils.getGoToSettingsListener;
+import static org.briarproject.hotspot.UiUtils.showDenialDialog;
+import static org.briarproject.hotspot.UiUtils.showRationale;
+
+/**
+ * This class ensures that the conditions to open a hotspot are fulfilled on
+ * API levels >= 29.
+ * <p>
+ * As soon as {@link #checkAndRequestConditions()} returns true,
+ * all conditions are fulfilled.
+ */
+@RequiresApi(29)
+class ConditionManager29 extends AbstractConditionManager {
+
+	private static final Logger LOG =
+			getLogger(ConditionManager29.class.getName());
+
+	private Permission locationPermission = Permission.UNKNOWN;
+
+	private final ActivityResultLauncher<String> locationRequest;
+	private final ActivityResultLauncher<Intent> wifiRequest;
+
+	ConditionManager29(ActivityResultCaller arc,
+			Consumer<Boolean> permissionUpdateCallback) {
+		super(permissionUpdateCallback);
+		locationRequest = arc.registerForActivityResult(
+				new RequestPermission(), granted -> {
+					onRequestPermissionResult(granted);
+					permissionUpdateCallback.accept(TRUE.equals(granted));
+				});
+		wifiRequest = arc.registerForActivityResult(
+				new StartActivityForResult(),
+				result -> permissionUpdateCallback
+						.accept(wifiManager.isWifiEnabled()));
+	}
+
+	@Override
+	void onStart() {
+		locationPermission = Permission.UNKNOWN;
+	}
+
+	private boolean areEssentialPermissionsGranted() {
+		if (LOG.isLoggable(INFO)) {
+			LOG.info(String.format("areEssentialPermissionsGranted(): " +
+							"locationPermission? %s, " +
+							"wifiManager.isWifiEnabled()? %b",
+					locationPermission,
+					wifiManager.isWifiEnabled()));
+		}
+		return locationPermission == Permission.GRANTED &&
+				wifiManager.isWifiEnabled();
+	}
+
+	@Override
+	boolean checkAndRequestConditions() {
+		if (areEssentialPermissionsGranted()) return true;
+
+		if (locationPermission == Permission.UNKNOWN) {
+			locationRequest.launch(ACCESS_FINE_LOCATION);
+			return false;
+		}
+
+		// If the location permission has been permanently denied, ask the
+		// user to change the setting
+		if (locationPermission == Permission.PERMANENTLY_DENIED) {
+			showDenialDialog(ctx, R.string.permission_location_title,
+					R.string.permission_hotspot_location_denied_body,
+					getGoToSettingsListener(ctx),
+					() -> permissionUpdateCallback.accept(false));
+			return false;
+		}
+
+		// Should we show the rationale for location permission?
+		if (locationPermission == Permission.SHOW_RATIONALE) {
+			showRationale(ctx, R.string.permission_location_title,
+					R.string.permission_hotspot_location_request_body,
+					this::requestPermissions,
+					() -> permissionUpdateCallback.accept(false));
+			return false;
+		}
+
+		// If Wifi is not enabled, we show the rationale for enabling Wifi?
+		if (!wifiManager.isWifiEnabled()) {
+			showRationale(ctx, R.string.wifi_settings_title,
+					R.string.wifi_settings_request_enable_body,
+					this::requestEnableWiFi,
+					() -> permissionUpdateCallback.accept(false));
+			return false;
+		}
+
+		// we shouldn't usually reach this point, but if we do, return false
+		// anyway to force a recheck. Maybe some condition changed in the
+		// meantime.
+		return false;
+	}
+
+	private void onRequestPermissionResult(@Nullable Boolean granted) {
+		if (granted != null && granted) {
+			locationPermission = Permission.GRANTED;
+		} else if (shouldShowRequestPermissionRationale(ctx,
+				ACCESS_FINE_LOCATION)) {
+			locationPermission = Permission.SHOW_RATIONALE;
+		} else {
+			locationPermission = Permission.PERMANENTLY_DENIED;
+		}
+	}
+
+	private void requestPermissions() {
+		locationRequest.launch(ACCESS_FINE_LOCATION);
+	}
+
+	private void requestEnableWiFi() {
+		wifiRequest.launch(new Intent(Settings.Panel.ACTION_WIFI));
+	}
+
+}
diff --git a/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java b/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java
index b608c84ec246a334d5c397f9117e088ded8d31db..f0aad6f16ee50cab492475c98d38cb6f2e7ce670 100644
--- a/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java
+++ b/app/src/main/java/org/briarproject/hotspot/HotspotFragment.java
@@ -1,6 +1,5 @@
 package org.briarproject.hotspot;
 
-import android.content.Intent;
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.view.LayoutInflater;
@@ -16,14 +15,12 @@ import org.briarproject.hotspot.HotspotState.HotspotStopped;
 import org.briarproject.hotspot.HotspotState.NetworkConfig;
 import org.briarproject.hotspot.HotspotState.StartingHotspot;
 
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
 import androidx.lifecycle.ViewModelProvider;
 
+import static android.os.Build.VERSION.SDK_INT;
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 import static org.briarproject.hotspot.HotspotManager.UNKNOWN_FREQUENCY;
@@ -33,30 +30,21 @@ import static org.briarproject.hotspot.QrCodeUtils.createWifiLoginString;
 public class HotspotFragment extends Fragment {
 
 	private MainViewModel viewModel;
-	private ConditionManager conditionManager;
 	private ImageView qrCode;
 	private TextView ssidView, passwordView, statusView;
 	private Button button, serverButton;
 	private boolean hotspotStarted = false;
 
-	private final ActivityResultLauncher<String> locationRequest =
-			registerForActivityResult(new RequestPermission(), granted -> {
-				conditionManager.onRequestPermissionResult(granted);
-				startWifiP2pHotspot();
-			});
-	private final ActivityResultLauncher<Intent> wifiRequest =
-			registerForActivityResult(new StartActivityForResult(), result -> {
-				conditionManager.onRequestWifiEnabledResult();
-				startWifiP2pHotspot();
-			});
+	private final AbstractConditionManager conditionManager = SDK_INT < 29 ?
+			new ConditionManager(this, this::onPermissionUpdate) :
+			new ConditionManager29(this, this::onPermissionUpdate);
 
 	@Override
 	public View onCreateView(LayoutInflater inflater, ViewGroup container,
 			Bundle savedInstanceState) {
 		viewModel = new ViewModelProvider(requireActivity())
 				.get(MainViewModel.class);
-		conditionManager = new ConditionManager(requireActivity(),
-				locationRequest, wifiRequest);
+		conditionManager.init(requireActivity());
 		return inflater.inflate(R.layout.fragment_hotspot, container, false);
 	}
 
@@ -145,25 +133,33 @@ public class HotspotFragment extends Fragment {
 	@Override
 	public void onStart() {
 		super.onStart();
-		conditionManager.resetPermissions();
+		conditionManager.onStart();
 	}
 
-	public void onButtonClick(View view) {
+	private void onButtonClick(View view) {
+		button.setEnabled(false);
 		if (hotspotStarted) {
-			button.setEnabled(false);
+			// the hotspot is currently started → stop it
 			viewModel.stopWifiP2pHotspot();
 		} else {
-			conditionManager.startConditionChecks();
+			// the hotspot is currently stopped → start it
+			startWifiP2pHotspotIfConditionsFulfilled();
 		}
 	}
 
-	private void startWifiP2pHotspot() {
+	private void startWifiP2pHotspotIfConditionsFulfilled() {
 		if (conditionManager.checkAndRequestConditions()) {
-			button.setEnabled(false);
 			viewModel.startWifiP2pHotspot();
 		}
 	}
 
+	private void onPermissionUpdate(boolean recheckPermissions) {
+		button.setEnabled(true);
+		if (recheckPermissions) {
+			startWifiP2pHotspotIfConditionsFulfilled();
+		}
+	}
+
 	public void onServerButtonClick(View view) {
 		getParentFragmentManager().beginTransaction()
 				.replace(R.id.fragment_container, new ServerFragment())
diff --git a/app/src/main/java/org/briarproject/hotspot/HotspotManager.java b/app/src/main/java/org/briarproject/hotspot/HotspotManager.java
index 187d384177ffd49f1ce68024c82f91b2e94d1081..b59214f721096c01e2d2d2802cd8f46fe54d3176 100644
--- a/app/src/main/java/org/briarproject/hotspot/HotspotManager.java
+++ b/app/src/main/java/org/briarproject/hotspot/HotspotManager.java
@@ -30,7 +30,7 @@ import static java.util.logging.Level.INFO;
 import static java.util.logging.Logger.getLogger;
 import static org.briarproject.hotspot.StringUtils.getRandomString;
 
-class HotspotManager implements ActionListener {
+class HotspotManager {
 
 	interface HotspotListener {
 
@@ -48,6 +48,7 @@ class HotspotManager implements ActionListener {
 
 	private static final Logger LOG = getLogger(HotspotManager.class.getName());
 
+	private static final int MAX_FRAMEWORK_ATTEMPTS = 5;
 	private static final int MAX_GROUP_INFO_ATTEMPTS = 5;
 	private static final int RETRY_DELAY_MILLIS = 1000;
 
@@ -85,26 +86,91 @@ class HotspotManager implements ActionListener {
 			return;
 		}
 		listener.onStartingHotspot();
+		acquireLock();
+		startWifiP2pFramework(1);
+	}
+
+	/**
+	 * As soon as Wifi is enabled, we try starting the WifiP2p framework.
+	 * If Wifi has just been enabled, it is possible that will fail. If that
+	 * happens we try again for MAX_FRAMEWORK_ATTEMPTS times after a delay of
+	 * RETRY_DELAY_MILLIS after each attempt.
+	 * <p>
+	 * Rationale: it can take a few milliseconds for WifiP2p to become available
+	 * after enabling Wifi. Depending on the API level it is possible to check this
+	 * using {@link WifiP2pManager#requestP2pState} or register a BroadcastReceiver
+	 * on the WIFI_P2P_STATE_CHANGED_ACTION to get notified when WifiP2p is really
+	 * available. Trying to implement a solution that works reliably using these
+	 * checks turned out to be a long rabbit-hole with lots of corner cases and
+	 * workarounds for specific situations.
+	 * Instead we now rely on this trial-and-error approach of just starting
+	 * the framework and retrying if it fails.
+	 * <p>
+	 * We'll realize that the framework is busy when the ActionListener passed
+	 * to {@link WifiP2pManager#createGroup} is called with onFailure(BUSY)
+	 */
+	void startWifiP2pFramework(int attempt) {
+		if (LOG.isLoggable(INFO))
+			LOG.info("startWifiP2pFramework attempt: " + attempt);
+		/*
+		 * It is important that we call WifiP2pManager#initialize again
+		 * for every attempt to starting the framework because otherwise,
+		 * createGroup() will continue to fail with a BUSY state.
+		 */
 		channel = wifiP2pManager.initialize(ctx, ctx.getMainLooper(), null);
 		if (channel == null) {
 			listener.onHotspotError(ctx.getString(R.string.no_wifi_direct));
 			return;
 		}
-		acquireLock();
+
+		ActionListener listener = new ActionListener() {
+
+			@Override
+			// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
+			public void onSuccess() {
+				requestGroupInfo(1);
+			}
+
+			@Override
+			// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
+			public void onFailure(int reason) {
+				LOG.info("onFailure: " + reason);
+				if (reason == BUSY) {
+					// WifiP2p not ready yet or hotspot already running
+					restartWifiP2pFramework(attempt);
+				} else if (reason == P2P_UNSUPPORTED) {
+					releaseHotspotWithError(ctx.getString(
+							R.string.start_callback_failed, "p2p unsupported"));
+				} else if (reason == ERROR) {
+					releaseHotspotWithError(ctx.getString(
+							R.string.start_callback_failed, "p2p error"));
+				} else if (reason == NO_SERVICE_REQUESTS) {
+					releaseHotspotWithError(ctx.getString(
+							R.string.start_callback_failed,
+							"no service requests"));
+				} else {
+					// all cases covered, in doubt set to error
+					releaseHotspotWithError(ctx.getString(
+							R.string.start_callback_failed_unknown, reason));
+				}
+			}
+		};
+
 		try {
 			if (SDK_INT >= 29) {
 				networkName = getNetworkName();
 				String passphrase = getPassphrase();
 				// TODO: maybe remove this in the production version
-				LOG.info("networkName: " + networkName);
+				if (LOG.isLoggable(INFO))
+					LOG.info("networkName: " + networkName);
 				WifiP2pConfig config = new WifiP2pConfig.Builder()
 						.setGroupOperatingBand(GROUP_OWNER_BAND_2GHZ)
 						.setNetworkName(networkName)
 						.setPassphrase(passphrase)
 						.build();
-				wifiP2pManager.createGroup(channel, config, this);
+				wifiP2pManager.createGroup(channel, config, listener);
 			} else {
-				wifiP2pManager.createGroup(channel, this);
+				wifiP2pManager.createGroup(channel, listener);
 			}
 		} catch (SecurityException e) {
 			// this should never happen, because we request permissions before
@@ -112,6 +178,17 @@ class HotspotManager implements ActionListener {
 		}
 	}
 
+	private void restartWifiP2pFramework(int attempt) {
+		LOG.info("retrying to start WifiP2p framework");
+		if (attempt < MAX_FRAMEWORK_ATTEMPTS) {
+			handler.postDelayed(() -> startWifiP2pFramework(attempt + 1),
+					RETRY_DELAY_MILLIS);
+		} else {
+			releaseHotspotWithError(
+					ctx.getString(R.string.stop_framework_busy));
+		}
+	}
+
 	@RequiresApi(29)
 	private String getNetworkName() {
 		return "DIRECT-" + getRandomString(2) + "-" +
@@ -122,6 +199,7 @@ class HotspotManager implements ActionListener {
 		return getRandomString(8);
 	}
 
+	@UiThread
 	void stopWifiP2pHotspot() {
 		if (channel == null) return;
 		wifiP2pManager.removeGroup(channel, new ActionListener() {
@@ -164,33 +242,6 @@ class HotspotManager implements ActionListener {
 		wifiLock.release();
 	}
 
-	@Override
-	// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
-	public void onSuccess() {
-		requestGroupInfo(1);
-	}
-
-	@Override
-	// Callback for wifiP2pManager#createGroup() during startWifiP2pHotspot()
-	public void onFailure(int reason) {
-		if (reason == BUSY)
-			// Hotspot already running
-			requestGroupInfo(1);
-		else if (reason == P2P_UNSUPPORTED)
-			releaseHotspotWithError(ctx.getString(
-					R.string.start_callback_failed, "p2p unsupported"));
-		else if (reason == ERROR)
-			releaseHotspotWithError(ctx.getString(
-					R.string.start_callback_failed, "p2p error"));
-		else if (reason == NO_SERVICE_REQUESTS)
-			releaseHotspotWithError(ctx.getString(
-					R.string.start_callback_failed, "no service requests"));
-		else
-			// all cases covered, in doubt set to error
-			releaseHotspotWithError(ctx.getString(
-					R.string.start_callback_failed_unknown, reason));
-	}
-
 	private void requestGroupInfo(int attempt) {
 		if (LOG.isLoggable(INFO))
 			LOG.info("requestGroupInfo attempt: " + attempt);
@@ -279,7 +330,7 @@ class HotspotManager implements ActionListener {
 	}
 
 	private void retryRequestingGroupInfo(int attempt) {
-		LOG.info("retrying");
+		LOG.info("retrying to request group info");
 		// On some devices we need to wait for the group info to become available
 		if (attempt < MAX_GROUP_INFO_ATTEMPTS) {
 			handler.postDelayed(() -> requestGroupInfo(attempt + 1),
diff --git a/app/src/main/java/org/briarproject/hotspot/UiUtils.java b/app/src/main/java/org/briarproject/hotspot/UiUtils.java
index 84959e353b532cbc6e797db52acaecf96e778183..551aa181e44c8749576e37e06e29ba7d97739bef 100644
--- a/app/src/main/java/org/briarproject/hotspot/UiUtils.java
+++ b/app/src/main/java/org/briarproject/hotspot/UiUtils.java
@@ -5,13 +5,17 @@ import android.content.DialogInterface;
 import android.content.Intent;
 import android.net.Uri;
 
+import androidx.annotation.StringRes;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.FragmentActivity;
+
 import static android.content.Intent.CATEGORY_DEFAULT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
 import static org.briarproject.hotspot.BuildConfig.APPLICATION_ID;
 
 class UiUtils {
 
-	public static DialogInterface.OnClickListener getGoToSettingsListener(
+	static DialogInterface.OnClickListener getGoToSettingsListener(
 			Context context) {
 		return (dialog, which) -> {
 			Intent i = new Intent();
@@ -23,4 +27,29 @@ class UiUtils {
 		};
 	}
 
+	static void showDenialDialog(FragmentActivity ctx, @StringRes int title,
+			@StringRes int body, DialogInterface.OnClickListener onOkClicked,
+			Runnable onDismiss) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+		builder.setTitle(title);
+		builder.setMessage(body);
+		builder.setPositiveButton(R.string.ok, onOkClicked);
+		builder.setNegativeButton(R.string.cancel,
+				(dialog, which) -> ctx.supportFinishAfterTransition());
+		builder.setOnDismissListener(dialog -> onDismiss.run());
+		builder.show();
+	}
+
+	static void showRationale(Context ctx, @StringRes int title,
+			@StringRes int body,
+			Runnable onContinueClicked, Runnable onDismiss) {
+		AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
+		builder.setTitle(title);
+		builder.setMessage(body);
+		builder.setNeutralButton(R.string.continue_button,
+				(dialog, which) -> onContinueClicked.run());
+		builder.setOnDismissListener(dialog -> onDismiss.run());
+		builder.show();
+	}
+
 }
diff --git a/app/src/main/res/layout/fragment_hotspot.xml b/app/src/main/res/layout/fragment_hotspot.xml
index 24dbe332043b5dda19a5e5db60b2d514dca2e9af..654603e44cd30c55f1f2237867b64ce86069f2c2 100644
--- a/app/src/main/res/layout/fragment_hotspot.xml
+++ b/app/src/main/res/layout/fragment_hotspot.xml
@@ -45,7 +45,9 @@
 			android:id="@+id/status"
 			android:layout_width="wrap_content"
 			android:layout_height="wrap_content"
-			android:textSize="16sp" />
+			android:layout_margin="16dp"
+			android:textSize="16sp"
+			tools:text="@string/stop_framework_busy" />
 
 		<Button
 			android:id="@+id/serverButton"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a2fdb737657919f89e7aeca61075ec90578cef3f..8db67ce71501a876ffe90968d2ac3614df54ffbf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,6 +25,7 @@
 	<string name="start_callback_no_group_info">Hotspot failed to start: no group info</string>
 	<string name="start_no_attempts_left">Hotspot failed to start with an unknown error</string>
 	<string name="stop_callback_failed">Unknown error while stopping hotspot (reason %d)</string>
+	<string name="stop_framework_busy">Unable to start the hotspot. If you have another hotspot running or are sharing your internet connection via Wifi, try stopping that and try again afterwards.</string>
 	<string name="hotspot_stopped">Hotspot stopped</string>
 	<string name="qr_code_description">QR code with Wi-Fi login details</string>
 	<string name="no_wifi_manager">Device does not support Wi-Fi</string>