Permissions Guide
This guide covers Bluetooth permission handling across iOS, Android, and Windows platforms.
Overview
Plugin.Bluetooth provides a unified permission model that adapts to platform-specific requirements:
- Permission Request Strategies: Choose when and how permissions are requested
- Platform-Specific Behavior: Understand how each platform handles permissions
- Runtime Permission Management: Check and request permissions programmatically
Permission Request Strategies
Plugin.Bluetooth offers three strategies for permission management, available for both scanning and connection operations:
RequestAutomatically (Default)
The library automatically requests permissions before performing operations.
// Permissions requested automatically
await scanner.StartScanningAsync();
// Or explicitly specify (same as default)
var options = new ScanningOptions
{
PermissionStrategy = PermissionRequestStrategy.RequestAutomatically
};
await scanner.StartScanningAsync(options);
When to use:
- Most applications (recommended default)
- Simple apps without complex permission flows
- When you want the library to handle permission dialogs
Platform behavior:
- Android: Shows permission dialog automatically before scanning/connecting
- iOS/macOS: No-op (CoreBluetooth shows system dialog automatically)
- Windows: Checks adapter state and requests radio access if needed
ThrowIfNotGranted
Take explicit control over permission requests. The library throws BluetoothPermissionException if permissions are not granted.
// Check permissions
if (!await scanner.HasScannerPermissionsAsync())
{
try
{
// Request permissions explicitly
await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException ex)
{
// Handle permission denial
await ShowPermissionDeniedDialog();
return;
}
}
// Start operation with explicit strategy
var options = new ScanningOptions
{
PermissionStrategy = PermissionRequestStrategy.ThrowIfNotGranted
};
try
{
await scanner.StartScanningAsync(options);
}
catch (BluetoothPermissionException)
{
// This shouldn't happen if you checked above, but handle just in case
}
When to use:
- Apps with custom permission request flows
- When you want to show custom UI before system dialogs
- Multi-step wizard-style permission flows
- When you need fine-grained error handling
Advantages:
- Complete control over permission timing
- Can show custom explanations before system dialogs
- Better user experience for permission-sensitive features
AssumeGranted
Skip all permission checks. Use only when you've already verified permissions elsewhere.
var options = new ScanningOptions
{
PermissionStrategy = PermissionRequestStrategy.AssumeGranted
};
await scanner.StartScanningAsync(options);
When to use:
- Testing and development
- When using external permission management libraries
- Apps with custom permission handling outside the library
Warning: Using
AssumeGrantedwithout actually having permissions will cause runtime errors:
- Android:
SecurityExceptionfrom native Bluetooth APIs- iOS: System will still show permission dialog (cannot be prevented)
- Windows: Operations may fail when accessing Bluetooth APIs
Permission APIs
Checking Permissions
Check if permissions are currently granted without triggering a request:
using Bluetooth.Abstractions.Scanning;
// Check scanner permissions
bool hasScannerPermissions = await scanner.HasScannerPermissionsAsync();
if (!hasScannerPermissions)
{
// Show explanation or request permissions
}
Platform behavior:
- Android: Checks
BLUETOOTH_SCAN(API 31+) or location permissions (older) - iOS/macOS: Checks Bluetooth authorization status
- Windows: Checks adapter availability and radio state
Requesting Permissions
Explicitly request permissions from the user:
try
{
// Request scanner permissions
await scanner.RequestScannerPermissionsAsync();
// Optional: Request background location (Android API 29-30 only)
await scanner.RequestScannerPermissionsAsync(
requireBackgroundLocation: true
);
}
catch (BluetoothPermissionException ex)
{
// Permission denied by user
Console.WriteLine($"Permission denied: {ex.Message}");
// Check inner exception for platform-specific details
if (ex.InnerException is SecurityException secEx)
{
// Android-specific handling
}
}
Platform behavior:
- Android: Shows system permission dialog; can be called multiple times
- iOS/macOS: Shows permission dialog on first call; subsequent denials require Settings app
- Windows: Checks adapter state and requests radio access if needed
Using Cancellation Tokens
Support cancellation for long-running permission requests:
private CancellationTokenSource _cts = new();
public async Task RequestPermissionsAsync()
{
try
{
await scanner.RequestScannerPermissionsAsync(
requireBackgroundLocation: false,
cancellationToken: _cts.Token
);
}
catch (OperationCanceledException)
{
Console.WriteLine("Permission request cancelled");
}
}
public void CancelPermissionRequest()
{
_cts.Cancel();
}
Platform-Specific Permission Behavior
iOS & MacCatalyst
Permission Model
iOS uses a simple authorized/not-authorized model managed entirely by CoreBluetooth:
// Check current authorization status
bool isAuthorized = await scanner.HasScannerPermissionsAsync();
// Request authorization (shows dialog on first call)
await scanner.RequestScannerPermissionsAsync();
Authorization States
On iOS, Bluetooth authorization can be in several states:
- Not Determined: User hasn't been asked yet
- Denied: User explicitly denied permission
- Authorized: Permission granted
- Restricted: Permission restricted by device policy
First-Time Request
When requesting Bluetooth permission for the first time:
┌─────────────────────────────────────────┐
│ "MyApp" Would Like to Use Bluetooth │
├─────────────────────────────────────────┤
│ This app uses Bluetooth to connect │
│ to nearby devices │
├─────────────────────────────────────────┤
│ Don't Allow │ OK │
└─────────────────────────────────────────┘
The message comes from NSBluetoothAlwaysUsageDescription in Info.plist.
Subsequent Requests
If the user denies permission:
- First denial: System dialog appears again on next request
- Repeated denials: iOS shows "Go to Settings" dialog
// User denied permission previously
try
{
await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException)
{
// Show dialog guiding user to Settings
await ShowOpenSettingsDialog(
"Bluetooth access is required. Please enable it in Settings > MyApp > Bluetooth"
);
}
Opening Settings
Guide users to Settings to re-enable permissions:
// iOS-specific: Open app settings
#if IOS || MACCATALYST
if (UIKit.UIApplication.SharedApplication.CanOpenUrl(
new Foundation.NSUrl("app-settings:")))
{
await UIKit.UIApplication.SharedApplication.OpenUrlAsync(
new Foundation.NSUrl("app-settings:"),
new UIKit.UIApplicationOpenUrlOptions()
);
}
#endif
Key Characteristics
- Permission dialog managed by system (CoreBluetooth)
- Cannot customize dialog appearance
RequestAutomaticallyis effectively a no-op (system always controls dialog)- Background scanning requires additional setup (see Platform Setup)
Android
Android's permission model is the most complex, varying significantly across API levels.
API Level Differences
| API Level | Required Permissions | Notes |
|---|---|---|
| 23-28 (Android 6-8.1) | BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION |
Location required for scanning |
| 29-30 (Android 10-11) | Above + ACCESS_BACKGROUND_LOCATION |
Additional permission for background |
| 31+ (Android 12+) | BLUETOOTH_SCAN, BLUETOOTH_CONNECT |
Location no longer required |
Modern Android (API 31+)
On Android 12 and above, Bluetooth permissions are separate from location:
// Check permissions
bool hasPermissions = await scanner.HasScannerPermissionsAsync();
// Request permissions (shows Bluetooth permission dialog)
await scanner.RequestScannerPermissionsAsync();
Permission Dialog:
┌─────────────────────────────────────────┐
│ Allow MyApp to find, connect to, and │
│ determine the relative position of │
│ nearby devices? │
├─────────────────────────────────────────┤
│ While using the app │
│ Only this time │
│ Don't allow │
└─────────────────────────────────────────┘
Legacy Android (API 23-30)
On Android 6-11, location permission is required for Bluetooth scanning:
// Check permissions (includes location)
bool hasPermissions = await scanner.HasScannerPermissionsAsync();
// Request permissions (shows location permission dialog)
await scanner.RequestScannerPermissionsAsync();
Why Location? Bluetooth device MAC addresses can be used to infer physical location, so Android requires location permission for scanning on these API levels.
Permission Dialog:
┌─────────────────────────────────────────┐
│ Allow MyApp to access this device's │
│ location? │
├─────────────────────────────────────────┤
│ While using the app │
│ Only this time │
│ Don't allow │
└─────────────────────────────────────────┘
Background Location (API 29-30)
For background scanning on Android 10-11, request background location:
var options = new ScanningOptions
{
RequireBackgroundLocation = true
};
await scanner.StartScanningAsync(options);
// Or request explicitly
await scanner.RequestScannerPermissionsAsync(
requireBackgroundLocation: true
);
Two-Step Process:
- First, user grants foreground location
- Then, system shows background location dialog
┌─────────────────────────────────────────┐
│ Allow MyApp to access this device's │
│ location all the time? │
├─────────────────────────────────────────┤
│ Allow all the time │
│ Allow only while using the app │
│ Don't allow │
└─────────────────────────────────────────┘
"Don't Ask Again" Handling
Android shows a "Don't ask again" checkbox after repeated denials:
try
{
await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException ex)
{
// Check if user selected "Don't ask again"
// On Android, subsequent requests will fail immediately
// Guide user to app settings
await ShowOpenSettingsDialog(
"Bluetooth permission is required. Please enable it in Settings > Apps > MyApp > Permissions"
);
}
Opening app settings on Android:
#if ANDROID
var intent = new Android.Content.Intent(
Android.Provider.Settings.ActionApplicationDetailsSettings
);
intent.SetData(Android.Net.Uri.Parse(
$"package:{Android.App.Application.Context.PackageName}"
));
Android.App.Application.Context.StartActivity(intent);
#endif
Connection Permissions (API 31+)
Connecting to devices on Android 12+ requires BLUETOOTH_CONNECT:
// Connection options with permission strategy
var options = new ConnectionOptions
{
PermissionStrategy = PermissionRequestStrategy.RequestAutomatically
};
try
{
await device.ConnectAsync(options);
}
catch (BluetoothPermissionException)
{
// Handle BLUETOOTH_CONNECT denial
}
Location Services Requirement (API 23-30)
On Android 6-11, location services must be enabled on the device:
// Check if location services are enabled (Android-specific)
#if ANDROID
var locationManager = Android.App.Application.Context.GetSystemService(
Android.Content.Context.LocationService
) as Android.Locations.LocationManager;
bool locationEnabled = locationManager?.IsLocationEnabled ?? false;
if (!locationEnabled)
{
// Guide user to enable location services
await ShowEnableLocationDialog(
"Please enable Location Services in device settings to scan for Bluetooth devices"
);
}
#endif
Windows
Windows has the simplest permission model for desktop applications.
Permission Model
Windows doesn't show permission dialogs for Bluetooth access in desktop apps:
// Check if Bluetooth adapter is available and enabled
bool isAvailable = await scanner.HasScannerPermissionsAsync();
if (!isAvailable)
{
// Adapter is not available or Bluetooth is off
await ShowEnableBluetoothDialog(
"Please enable Bluetooth in Windows Settings to scan for devices"
);
}
Adapter States
The Bluetooth adapter can be in several states:
- Powered On: Ready to use
- Powered Off: Bluetooth radio is disabled
- Not Available: No Bluetooth hardware or drivers not installed
- Unauthorized: Capability not declared in manifest
Requesting Radio Access
Plugin.Bluetooth can request radio access if Bluetooth is turned off:
try
{
await scanner.RequestScannerPermissionsAsync();
// This may prompt user to turn on Bluetooth radio
}
catch (BluetoothPermissionException ex)
{
// Adapter not available or user declined to enable radio
Console.WriteLine($"Bluetooth not available: {ex.Message}");
}
Opening Settings
Guide users to Windows Settings to enable Bluetooth:
#if WINDOWS
// Open Bluetooth settings
await Windows.System.Launcher.LaunchUriAsync(
new Uri("ms-settings:bluetooth")
);
#endif
Key Characteristics
- No runtime permission dialogs for desktop apps
- Must declare
bluetoothcapability inPackage.appxmanifest - Adapter must be physically present and drivers installed
- Can request radio access but cannot install Bluetooth adapter in code
Permission Scenarios
Scenario 1: First-Time App Launch
Recommended flow for apps using Bluetooth on first launch:
public async Task InitializeBluetoothAsync()
{
// Check if permissions are already granted
bool hasPermissions = await scanner.HasScannerPermissionsAsync();
if (!hasPermissions)
{
// Show educational UI explaining why Bluetooth is needed
await ShowBluetoothExplanationAsync();
try
{
// Request permissions
await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException ex)
{
// User denied permission
await ShowPermissionDeniedDialogAsync();
return;
}
}
// Permissions granted - start scanning
await scanner.StartScanningAsync();
}
private async Task ShowBluetoothExplanationAsync()
{
await DisplayAlert(
"Bluetooth Access",
"This app uses Bluetooth to connect to nearby fitness devices and track your activities.",
"OK"
);
}
Scenario 2: Permission Denied
Handle gracefully when users deny permission:
public async Task HandlePermissionDeniedAsync()
{
bool shouldRetry = await DisplayAlert(
"Permission Required",
"Bluetooth access is required to connect to your devices. Without this permission, the app cannot function properly.",
"Go to Settings",
"Cancel"
);
if (shouldRetry)
{
// Open platform-specific settings
await OpenAppSettingsAsync();
}
}
private async Task OpenAppSettingsAsync()
{
#if IOS || MACCATALYST
await OpenIOSSettingsAsync();
#elif ANDROID
OpenAndroidSettings();
#elif WINDOWS
await OpenWindowsBluetoothSettingsAsync();
#endif
}
Scenario 3: Runtime Permission Revocation
Monitor permission changes while app is running:
public class BluetoothService
{
private readonly IBluetoothScanner _scanner;
private Timer _permissionCheckTimer;
public event EventHandler<bool> PermissionStatusChanged;
public BluetoothService(IBluetoothScanner scanner)
{
_scanner = scanner;
// Check permissions periodically
_permissionCheckTimer = new Timer(CheckPermissions, null,
TimeSpan.Zero, TimeSpan.FromSeconds(5));
}
private async void CheckPermissions(object state)
{
bool hasPermissions = await _scanner.HasScannerPermissionsAsync();
PermissionStatusChanged?.Invoke(this, hasPermissions);
if (!hasPermissions && _scanner.IsRunning)
{
// Permissions were revoked while scanning
await _scanner.StopScanningAsync();
await ShowPermissionRevokedDialogAsync();
}
}
}
Scenario 4: Background Scanning (Android)
Request background location permission for background scanning:
public async Task EnableBackgroundScanningAsync()
{
// First, ensure foreground permissions are granted
if (!await scanner.HasScannerPermissionsAsync())
{
await scanner.RequestScannerPermissionsAsync();
}
#if ANDROID
// On Android 10-11, request background location
if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Q &&
Android.OS.Build.VERSION.SdkInt < Android.OS.BuildVersionCodes.S)
{
var options = new ScanningOptions
{
RequireBackgroundLocation = true
};
try
{
await scanner.StartScanningAsync(options);
}
catch (BluetoothPermissionException)
{
await DisplayAlert(
"Background Permission Required",
"To scan for devices in the background, please allow 'All the time' location access.",
"OK"
);
}
}
#endif
}
Best Practices
1. Request Permissions Just-In-Time
Don't request permissions until the user performs an action that requires them:
// ❌ Bad: Request on app launch
public override async void OnAppearing()
{
await scanner.RequestScannerPermissionsAsync();
}
// ✅ Good: Request when user taps "Scan" button
private async void OnScanButtonClicked(object sender, EventArgs e)
{
try
{
await scanner.StartScanningAsync(); // Uses RequestAutomatically
}
catch (BluetoothPermissionException)
{
await ShowPermissionRequiredDialog();
}
}
2. Provide Context Before Requesting
Show explanation before triggering permission dialogs:
private async Task ScanForDevicesAsync()
{
if (!await scanner.HasScannerPermissionsAsync())
{
// Explain why permission is needed
bool accepted = await DisplayAlert(
"Bluetooth Access Needed",
"To find your fitness tracker, we need permission to scan for nearby Bluetooth devices.",
"Continue",
"Cancel"
);
if (!accepted) return;
// Now request permission
try
{
await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException)
{
await ShowPermissionDeniedDialog();
return;
}
}
await scanner.StartScanningAsync();
}
3. Handle Graceful Degradation
Provide alternative functionality when permissions are denied:
private async Task InitializeAsync()
{
if (!await scanner.HasScannerPermissionsAsync())
{
// Show UI explaining limited functionality
ShowLimitedModeUI();
return;
}
// Full functionality available
ShowFullFeatureUI();
await scanner.StartScanningAsync();
}
private void ShowLimitedModeUI()
{
// Show read-only mode, manual device entry, etc.
}
4. Test Permission Flows
Test all permission scenarios:
- First-time request
- Permission granted
- Permission denied
- Permission denied with "Don't ask again" (Android)
- Permission revoked while app is running
- Permission re-granted after denial
5. Platform-Specific Handling
Use conditional compilation for platform-specific behavior:
private async Task HandlePermissionDenialAsync()
{
#if ANDROID
// Android: Check for "Don't ask again"
// Guide to settings if needed
#elif IOS || MACCATALYST
// iOS: Always guide to Settings after first denial
await OpenIOSSettingsAsync();
#elif WINDOWS
// Windows: Check if adapter is available
await OpenBluetoothSettingsAsync();
#endif
}
Troubleshooting
Permission Requests Not Showing
iOS:
- Verify
NSBluetoothAlwaysUsageDescriptionis inInfo.plist - Clean and rebuild project
- Check that description string is not empty
Android:
- Verify permissions are declared in
AndroidManifest.xml - Check target SDK version matches permission requirements
- Ensure location services are enabled (API 23-30)
Windows:
- Verify
bluetoothcapability inPackage.appxmanifest - Check that Bluetooth adapter is available
Permission Requests Failing Silently
Enable detailed exception logging:
try
{
await scanner.RequestScannerPermissionsAsync();
}
catch (BluetoothPermissionException ex)
{
// Log full exception details
Console.WriteLine($"Permission exception: {ex.Message}");
Console.WriteLine($"Inner exception: {ex.InnerException?.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
Permissions Granted But Operations Fail
Android API 23-30:
- Check if location services are enabled on device
- Verify GPS/location is turned on in device settings
Windows:
- Check if Bluetooth radio is powered on
- Verify Bluetooth drivers are installed
Next Steps
- Getting Started Guide - Basic usage and code examples
- Platform Setup Guide - Platform-specific manifest configuration
- Architecture Guidelines - Understanding library design