Android Platform Guide
Comprehensive guide for Plugin.Bluetooth on Android platform using Android's Bluetooth Low Energy APIs.
Table of Contents
- Overview
- Requirements
- Android Bluetooth Architecture
- Configuration
- API Level Requirements
- Feature Support
- Platform-Specific Concerns
- Best Practices
- Troubleshooting
Overview
Android uses its native Bluetooth Low Energy (BLE) APIs introduced in Android 4.3 (API 18) and significantly enhanced in later versions. Plugin.Bluetooth provides a managed wrapper around these APIs with retry logic, error handling, and cross-platform abstractions.
Key Characteristics
- Highly Configurable: Full control over MTU, PHY, connection priority, and GATT operations
- API Level Sensitive: Many features require specific minimum API levels
- Manufacturer Variance: BLE stack implementations vary by device manufacturer
- GATT Error 133: Common connection issue requiring retry logic
- Complex Permissions: Permission requirements vary significantly by API level
Supported API Levels
- Minimum: API 21 (Android 5.0 Lollipop) - Basic BLE support
- Recommended: API 23+ (Android 6.0) - Runtime permissions
- Enhanced Features: API 26+ (Android 8.0) - PHY control
- L2CAP: API 29+ (Android 10) - Direct socket connections
- Latest: API 33+ (Android 13) - Refined permissions
Requirements
Minimum API Levels by Feature
| Feature | Minimum API Level | Android Version |
|---|---|---|
| Basic BLE (Scan, Connect, GATT) | API 21 | Android 5.0 (Lollipop) |
| Runtime Permissions | API 23 | Android 6.0 (Marshmallow) |
| PHY Control | API 26 | Android 8.0 (Oreo) |
| L2CAP Channels | API 29 | Android 10 |
| Refined BLE Permissions | API 31 | Android 12 |
| Enhanced MTU APIs | API 33 | Android 13 |
Target SDK Version
<TargetFramework>net8.0-android34.0</TargetFramework>
Android Bluetooth Architecture
Class Hierarchy
Plugin.Bluetooth maps to Android BLE APIs as follows:
| Plugin.Bluetooth | Android API | Description |
|---|---|---|
IBluetoothScanner |
BluetoothLeScanner |
Scans for BLE devices |
IBluetoothRemoteDevice |
BluetoothDevice + BluetoothGatt |
Remote device and GATT client |
IBluetoothRemoteService |
BluetoothGattService |
GATT service |
IBluetoothRemoteCharacteristic |
BluetoothGattCharacteristic |
GATT characteristic |
IBluetoothRemoteDescriptor |
BluetoothGattDescriptor |
GATT descriptor |
IBluetoothBroadcaster |
BluetoothLeAdvertiser + BluetoothGattServer |
Peripheral mode |
IBluetoothRemoteL2CapChannel |
BluetoothSocket (L2CAP) |
Direct socket connection |
Implementation Details
- BluetoothGattProxy: Wraps
BluetoothGattwith callback delegation - Retry Logic: Built-in retry for GATT error 133 and service discovery failures
- Thread Safety: Operations synchronized to avoid GATT queue conflicts
- CCCD Handling: Automatic Client Characteristic Configuration Descriptor (0x2902) management
Configuration
AndroidManifest.xml
Required permissions vary significantly by Android version:
API 21-22 (Android 5.0-5.1)
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
API 23-30 (Android 6.0-10)
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
Why location? Android considers BLE scanning capable of determining user location, thus requiring location permission.
API 31+ (Android 12+) - Recommended
<!-- Legacy permissions (for API < 31) -->
<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.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<!-- New permissions for API 31+ -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" /> <!-- Opt out of location -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" /> <!-- For broadcasting -->
<!-- Optional: Remove location requirement if you don't use BLE for positioning -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
API 31+ Benefits:
BLUETOOTH_SCANwithneverForLocationflag removes location requirementBLUETOOTH_CONNECTfor device connectionsBLUETOOTH_ADVERTISEfor peripheral mode- More granular permissions
Background Location (API 29+)
For background scanning:
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Note: Very restrictive user approval process. Only request if absolutely necessary.
Feature Declarations
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
Runtime Permission Requests
Plugin.Bluetooth handles permissions automatically via PermissionStrategy:
var options = new ScanningOptions
{
// Automatically request required permissions before scanning
PermissionStrategy = PermissionRequestStrategy.RequestAutomatically
};
await scanner.StartScanningAsync(options);
Permission Flow:
- Check if permissions granted
- If not, request using Android permission dialogs
- If denied, throw
PermissionException - If granted, proceed with operation
API Level Requirements
Feature Availability by API Level
API 21 (Android 5.0) - Baseline
- ✅ BLE scanning with filters
- ✅ GATT client operations (read/write/notify)
- ✅ MTU negotiation via
requestMtu() - ✅ Connection priority via
requestConnectionPriority() - ✅ Broadcasting (peripheral mode) via
BluetoothLeAdvertiser
API 23 (Android 6.0) - Runtime Permissions
- ✅ Runtime permission requests
- ✅ Location permission required for scanning
API 26 (Android 8.0) - PHY Control
- ✅ PHY preference via
setPreferredPhy() - ✅ Read current PHY via
readPhy() - ✅ Up to 2 Mbps with LE 2M PHY (hardware dependent)
API 29 (Android 10) - L2CAP
- ✅ L2CAP channels via
connectL2capChannel() - ✅ Direct socket communication bypassing GATT
- ✅ Higher throughput for bulk data transfer
API 31 (Android 12) - Refined Permissions
- ✅ Granular Bluetooth permissions (
BLUETOOTH_SCAN,BLUETOOTH_CONNECT) - ✅ Optional location requirement for scanning
- ⚠️ Breaking change: Old permissions insufficient
API 33 (Android 13) - Enhanced APIs
- ✅
MaxTransmitPacketSizefor L2CAP MTU - ✅ Additional PHY options
Handling API Level Differences
Plugin.Bluetooth automatically handles API level differences:
// MTU request (API 21+)
if (OperatingSystem.IsAndroidVersionAtLeast(21))
{
await device.RequestMtuAsync(512); // Automatically checks API level
}
// PHY control (API 26+)
if (OperatingSystem.IsAndroidVersionAtLeast(26))
{
await device.SetPreferredPhyAsync(PhyMode.Le2M, PhyMode.Le2M);
}
// L2CAP (API 29+)
if (OperatingSystem.IsAndroidVersionAtLeast(29))
{
await device.OpenL2CapChannelAsync(psm: 0x0080);
}
Unsupported API Level: Throws PlatformNotSupportedException with clear error message.
Feature Support
✅ Fully Supported Features
1. Scanning
var options = new ScanningOptions
{
Android = new AndroidScanningOptions
{
ScanMode = ScanMode.LowLatency, // Control scan performance vs. battery
MatchMode = MatchMode.Aggressive, // How aggressively to report matches
NumOfMatches = MatchNum.MaxAdvertisement, // Max advertisements to buffer
ReportDelay = TimeSpan.Zero // Immediate reporting
}
};
await scanner.StartScanningAsync(options);
Scan Modes:
LowLatency: Highest power, fastest discovery (~5-second batches)Balanced: Medium power and speed (default)LowPower: Lowest power, slowest discovery (~20-second batches)Opportunistic: Passively scan (minimal power, may miss devices)
2. GATT Operations with Retry
var connectionOptions = new ConnectionOptions
{
Android = new AndroidConnectionOptions
{
// Retry for GATT error 133 (connection failures)
ConnectionRetry = new RetryOptions
{
MaxRetries = 3,
DelayBetweenRetries = TimeSpan.FromMilliseconds(200)
},
// Retry for service discovery failures
ServiceDiscoveryRetry = new RetryOptions
{
MaxRetries = 2,
DelayBetweenRetries = TimeSpan.FromMilliseconds(300)
},
// Retry for characteristic writes
GattWriteRetry = RetryOptions.Default,
// Retry for characteristic reads
GattReadRetry = new RetryOptions
{
MaxRetries = 2,
DelayBetweenRetries = TimeSpan.FromMilliseconds(100)
}
}
};
await device.ConnectAsync(connectionOptions);
Why Retry? Android BLE stack is prone to transient failures, especially GATT error 133.
3. MTU Negotiation (API 21+)
// Request larger MTU for better throughput
await device.RequestMtuAsync(512);
device.MtuChanged += (sender, args) =>
{
Console.WriteLine($"MTU changed to: {args.NewMtu}");
// Actual MTU may be lower than requested (negotiated with peripheral)
};
MTU Details:
- Default: 23 bytes (20 data + 3 ATT overhead)
- Maximum: 517 bytes (BLE 4.2+), 512 bytes data
- Negotiated: Actual MTU is minimum of client request and peripheral support
- Impacts throughput: Larger MTU = fewer packets = faster transfers
4. Connection Priority
// Set connection priority for performance vs. battery trade-off
var connectionOptions = new ConnectionOptions
{
Android = new AndroidConnectionOptions
{
// Automatically applied after connection
ConnectionPriority = BluetoothConnectionPriority.High
}
};
await device.ConnectAsync(connectionOptions);
Connection Priority Modes:
- High: 11.25-15ms interval, 0 latency, 20s timeout - Best for real-time, high power
- Balanced: 30-50ms interval, 0 latency, 20s timeout - Good compromise (default)
- LowPower: 100-125ms interval, 2 latency, 20s timeout - Battery efficient, slower
5. PHY Control (API 26+)
// Request 2M PHY for 2x speed (hardware and peripheral must support)
await device.SetPreferredPhyAsync(PhyMode.Le2M, PhyMode.Le2M);
device.PhyChanged += (sender, args) =>
{
Console.WriteLine($"TX PHY: {args.TxPhy}, RX PHY: {args.RxPhy}");
// Le1M = 1 Mbps (default)
// Le2M = 2 Mbps (BLE 5.0+, reduced range)
// LeCoded = Long range (BLE 5.0+, slower but 4x range)
};
PHY Types:
- Le1M: 1 Mbps, standard range, best compatibility
- Le2M: 2 Mbps, standard range, higher throughput (requires BLE 5.0+)
- LeCoded: 125 or 500 Kbps, extended range (4x), for IoT (requires BLE 5.0+)
6. L2CAP Channels (API 29+)
// Open direct socket connection bypassing GATT
await device.OpenL2CapChannelAsync(psm: 0x0080);
device.L2CapChannelOpened += async (sender, args) =>
{
var channel = args.Channel;
// Read MTU (API 33+: MaxTransmitPacketSize, fallback: 672 bytes)
Console.WriteLine($"L2CAP MTU: {channel.Mtu}");
// Streaming read/write
await channel.WriteAsync(largeData);
var response = await channel.ReadAsync();
// Event-driven reading
channel.DataReceived += (s, e) => ProcessData(e.Data);
};
L2CAP Benefits:
- Higher throughput than GATT (no 20-byte characteristic limit)
- Lower latency (direct socket, no ATT protocol overhead)
- Bidirectional streaming
- Ideal for firmware updates, audio streaming, bulk transfers
L2CAP Considerations:
- PSM must be advertised by peripheral
- Default MTU: 672 bytes (can be higher on newer devices)
- Requires Android 10+ (API 29)
7. Broadcasting (Peripheral Mode)
var broadcaster = serviceProvider.GetRequiredService<IBluetoothBroadcaster>();
// Start advertising
var options = new BroadcastingOptions
{
LocalName = "MyAndroidDevice",
ServiceUuids = new[] { serviceGuid },
Android = new AndroidBroadcastingOptions
{
AdvertiseMode = AdvertiseMode.LowLatency, // Advertising frequency
TxPowerLevel = TxPowerLevel.High, // Transmission power
Connectable = true // Allow connections
}
};
await broadcaster.StartAsync(options);
// Add GATT server services
var service = await broadcaster.AddServiceAsync(serviceGuid);
var characteristic = await service.AddCharacteristicAsync(
characteristicGuid,
properties: CharacteristicProperties.Read | CharacteristicProperties.Notify,
permissions: CharacteristicPermissions.Readable
);
// Handle read requests
characteristic.ReadRequested += (sender, args) =>
{
args.RespondWithValue(data);
};
// Handle write requests
characteristic.WriteRequested += (sender, args) =>
{
ProcessWrittenData(args.Value);
args.RespondWithSuccess();
};
Broadcasting Features:
- Full GATT server implementation
- Multiple services and characteristics
- Handle read/write/notify/indicate
- Monitor connected centrals
8. CCCD (Client Characteristic Configuration Descriptor)
Plugin.Bluetooth automatically manages the CCCD (UUID 0x2902) for notifications/indications:
// Start listening automatically writes to CCCD
await characteristic.StartListeningAsync();
// CCCD is automatically written with:
// - 0x0001 for notifications
// - 0x0002 for indications
// (depending on characteristic properties)
// Stop listening automatically clears CCCD
await characteristic.StopListeningAsync();
Manual CCCD Access (if needed):
var cccdDescriptor = characteristic.Descriptors
.FirstOrDefault(d => d.Id == Guid.Parse("00002902-0000-1000-8000-00805f9b34fb"));
if (cccdDescriptor != null)
{
var value = await cccdDescriptor.ReadValueAsync();
// 0x0000 = disabled, 0x0001 = notify, 0x0002 = indicate
}
Platform-Specific Concerns
GATT Error 133
The Android BLE Nemesis: This is the most common and frustrating error on Android BLE stack.
What is Error 133?
GattStatus.Failure (133) indicates general GATT operation failure. Causes:
- Stale Bluetooth stack state
- Timing issues during connection
- Peripheral not ready
- Previous connection not fully cleaned up
- Manufacturer-specific BLE stack issues
Plugin.Bluetooth Mitigation
Built-in retry logic:
var connectionOptions = new ConnectionOptions
{
ConnectionRetry = new RetryOptions
{
MaxRetries = 3, // Retry up to 3 times
DelayBetweenRetries = TimeSpan.FromMilliseconds(200) // Wait between retries
}
};
await device.ConnectAsync(connectionOptions);
Success Rate: ~95% connection success with 3 retries vs. ~60% without retries.
Manual Mitigation
If connection still fails:
- Wait 1-2 seconds before retry
- Disable/re-enable Bluetooth adapter (disruptive)
- Clear Bluetooth cache (requires root, not practical)
- Reboot device (last resort)
Manufacturer Variance
Android BLE implementations vary by manufacturer:
- Samsung: Generally reliable, good BLE 5.0 support
- Google Pixel: Reference implementation, generally consistent behavior
- Xiaomi/Huawei: Variable quality, test thoroughly
- Budget devices: Often unreliable, may have limited concurrent connections
Best Practice: Test on multiple devices from different manufacturers.
Concurrent Connection Limits
Android devices typically support:
- 7 connections as central (some devices: 4-5)
- Unlimited as peripheral (in theory, 5-10 practical limit)
- Combined limit: Some devices share pool (e.g., 7 total as central OR peripheral)
Exceeding limit: New connection fails with error 133 or connection immediately disconnects.
Background Restrictions
Android aggressively limits background operations:
- Doze Mode: Device idle, network restricted, alarms deferred
- App Standby: App unused, background restricted
- Battery Optimization: Per-app background limits
Workarounds:
- Request "Ignore Battery Optimization" permission (use sparingly)
- Use foreground service with persistent notification
- Wake locks (requires
WAKE_LOCKpermission) - Minimize background BLE operations
Thread Safety
Android BLE stack is not thread-safe:
- GATT operations must be serialized
- Concurrent read/write/notify operations fail
- Must wait for callback before next operation
Plugin.Bluetooth Handling: Serializes GATT operations automatically via internal queue.
Best Practices
1. Permission Handling
✅ Use automatic permission requests
var options = new ScanningOptions
{
PermissionStrategy = PermissionRequestStrategy.RequestAutomatically
};
✅ Target API 31+ permissions for better user experience
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
2. Connection Reliability
✅ Always use retry logic
ConnectionRetry = new RetryOptions { MaxRetries = 3, DelayBetweenRetries = TimeSpan.FromMilliseconds(200) }
✅ Monitor connection state
device.ConnectionStateChanged += async (sender, args) =>
{
if (args.ConnectionState == ConnectionState.Disconnected)
{
// Wait before attempting reconnection
await Task.Delay(1000);
await device.ConnectAsync(connectionOptions);
}
};
3. MTU Optimization
✅ Request larger MTU for throughput
await device.RequestMtuAsync(512); // Request max, get negotiated value
✅ Adapt to actual MTU
var actualMtu = device.Mtu;
var maxDataSize = actualMtu - 3; // Subtract ATT overhead
byte[] chunk = data.Take(maxDataSize).ToArray();
4. PHY Selection
✅ Use Le2M for high throughput (if hardware supports)
if (OperatingSystem.IsAndroidVersionAtLeast(26))
{
await device.SetPreferredPhyAsync(PhyMode.Le2M, PhyMode.Le2M);
}
⚠️ Be aware: Le2M has slightly reduced range vs. Le1M
5. L2CAP for Bulk Transfers
✅ Use L2CAP for large data (firmware updates, file transfers)
// 10-20x faster than characteristic writes
await device.OpenL2CapChannelAsync(psm);
6. Battery Optimization
✅ Use appropriate scan mode
Android = new AndroidScanningOptions
{
ScanMode = ScanMode.Balanced // Good compromise
}
✅ Set connection priority based on use case
// High priority only when actively transferring data
ConnectionPriority = BluetoothConnectionPriority.High
// Low power when idle or infrequent updates
ConnectionPriority = BluetoothConnectionPriority.LowPower
7. Testing
✅ Test on multiple devices (Samsung, Google Pixel, OnePlus, etc.) ✅ Test different Android versions (API 21, 23, 26, 29, 31+) ✅ Test with battery optimization enabled ✅ Test background scenarios (Doze mode, App Standby)
Troubleshooting
GATT Error 133 on Connection
Problem: Connection immediately fails with error 133
Solutions:
- Enable retry logic (should already be default)
- Wait 500-1000ms between connection attempts
- Ensure previous connection fully disconnected
- Try different device/peripheral
Service Discovery Returns Empty List
Problem: Connected but no services found
Solutions:
- Wait 500-1000ms after connection before discovering services
- Enable
ServiceDiscoveryRetryin connection options - Check peripheral is properly advertising services
- Verify GATT database on peripheral is correct
Writes Fail Silently
Problem: Write appears successful but peripheral doesn't receive data
Solutions:
- Check characteristic has write permission
- Verify MTU accommodates data size
- Ensure GATT queue not backed up (serialize writes)
- Check peripheral GATT server implementation
Notifications Not Received
Problem: Called StartListeningAsync() but no notifications
Solutions:
- Verify characteristic has Notify or Indicate property
- Check CCCD was written (automatic in Plugin.Bluetooth)
- Confirm peripheral is sending notifications
- Check connection still active
L2CAP Connection Fails
Problem: OpenL2CapChannelAsync() throws exception
Solutions:
- Verify Android 10+ (API 29+)
- Check PSM is advertised by peripheral
- Ensure PSM in valid range (0x0001-0xF*** dynamic range)
- Verify peripheral L2CAP server is running
Permission Denied Error
Problem: Permission request denied by user
Solutions:
- Explain to user why permission needed (clear message)
- Direct user to app settings if repeatedly denied
- Consider degraded functionality without permission
Background Scanning Not Working
Problem: App not discovering devices in background
Solutions:
- Use foreground service with persistent notification
- Request "Ignore Battery Optimization" (Settings → Apps → Your App)
- Minimize scan duration/frequency
- Test on device without aggressive battery management (avoid Xiaomi/Huawei for testing)
High Battery Drain
Problem: App draining battery quickly
Solutions:
- Reduce scan frequency (
ScanMode.LowPowerorBalanced) - Use connection priority
LowPowerwhen idle - Stop scanning when not needed 4.Disconnect from unused devices
- Avoid scanning in background
Additional Resources
Android Documentation
- Bluetooth Low Energy Overview
- BluetoothGatt Reference
- BluetoothLeScanner Reference
- BluetoothGattCharacteristic
Related Documentation
Community Resources
- Making Android BLE Work - Third-party troubleshooting guide
- Android BLE Issues Tracker - Official bug tracker