Iris Classon
Iris Classon - In Love with Code

BLE Scanning on Android in .NET MAUI

In my previous post I mentioned scan modes briefly, mostly in the context of why RSSI behaves the way it does on Android. A few people asked for a more complete example, so here is a MAUI-ready Android service you can drop in.

BluetoothLeScanner gets the scan settings, and ScanSettings.Builder().SetScanMode(ScanMode.LowLatency) is the bit that enables low-latency scanning. In .NET for Android, those settings are passed into StartScan(...). Android’s BLE docs also recommend stopping scans promptly because continuous scanning is expensive on battery.

1. Shared interface

Create this somewhere in shared code, for example Services/IBleScanner.cs.

public interface IBleScanner
{
    event EventHandler<BleDeviceDiscoveredEventArgs>? DeviceDiscovered;

    bool IsScanning { get; }

    Task StartScanningAsync(CancellationToken cancellationToken = default);
    Task StopScanningAsync();
}

public sealed class BleDeviceDiscoveredEventArgs : EventArgs
{
    public BleDeviceDiscoveredEventArgs(string id, string? name, int rssi)
    {
        Id = id;
        Name = name;
        Rssi = rssi;
    }

    public string Id { get; }
    public string? Name { get; }
    public int Rssi { get; }
}

2. Android implementation

Put this in Platforms/Android/BleScanner.android.cs.

using Android.Bluetooth;
using Android.Bluetooth.LE;
using Android.Content;
using Microsoft.Maui.ApplicationModel;
using Application = Android.App.Application;

public sealed class BleScanner : Java.Lang.Object, IBleScanner
{
    private readonly BluetoothManager? _bluetoothManager;
    private BluetoothAdapter? _adapter;
    private BluetoothLeScanner? _scanner;
    private ScanCallbackImpl? _callback;

    public event EventHandler<BleDeviceDiscoveredEventArgs>? DeviceDiscovered;

    public bool IsScanning { get; private set; }

    public BleScanner()
    {
        _bluetoothManager =
            Application.Context.GetSystemService(Context.BluetoothService) as BluetoothManager;

        _adapter = _bluetoothManager?.Adapter;
        _scanner = _adapter?.BluetoothLeScanner;
    }

    public Task StartScanningAsync(CancellationToken cancellationToken = default)
    {
        EnsureBluetoothReady();

        if (IsScanning)
            return Task.CompletedTask;

        _callback = new ScanCallbackImpl(args =>
        {
            MainThread.BeginInvokeOnMainThread(() =>
            {
                DeviceDiscovered?.Invoke(this, args);
            });
        });

        var settings = new ScanSettings.Builder()
            .SetScanMode(Android.Bluetooth.LE.ScanMode.LowLatency)
            .Build();

        var filters = new List<ScanFilter>();

        _scanner!.StartScan(filters, settings, _callback);
        IsScanning = true;

        if (cancellationToken.CanBeCanceled)
        {
            cancellationToken.Register(() =>
            {
                _ = StopScanningAsync();
            });
        }

        return Task.CompletedTask;
    }

    public Task StopScanningAsync()
    {
        if (!IsScanning || _scanner == null || _callback == null)
            return Task.CompletedTask;

        _scanner.StopScan(_callback);
        IsScanning = false;
        _callback = null;

        return Task.CompletedTask;
    }

    private void EnsureBluetoothReady()
    {
        _adapter ??= _bluetoothManager?.Adapter;
        _scanner ??= _adapter?.BluetoothLeScanner;

        if (_bluetoothManager == null || _adapter == null || _scanner == null)
            throw new InvalidOperationException("Bluetooth LE scanning is not available on this device.");

        if (!_adapter.IsEnabled)
            throw new InvalidOperationException("Bluetooth is turned off.");
    }

    private sealed class ScanCallbackImpl : ScanCallback
    {
        private readonly Action<BleDeviceDiscoveredEventArgs> _onDeviceDiscovered;

        public ScanCallbackImpl(Action<BleDeviceDiscoveredEventArgs> onDeviceDiscovered)
        {
            _onDeviceDiscovered = onDeviceDiscovered;
        }

        public override void OnScanResult(ScanCallbackType callbackType, ScanResult? result)
        {
            if (result?.Device == null)
                return;

            var args = new BleDeviceDiscoveredEventArgs(
                id: result.Device.Address ?? string.Empty,
                name: result.Device.Name,
                rssi: result.Rssi);

            _onDeviceDiscovered(args);
        }

        public override void OnBatchScanResults(IList<ScanResult>? results)
        {
            if (results == null)
                return;

            foreach (var result in results)
            {
                if (result?.Device == null)
                    continue;

                var args = new BleDeviceDiscoveredEventArgs(
                    id: result.Device.Address ?? string.Empty,
                    name: result.Device.Name,
                    rssi: result.Rssi);

                _onDeviceDiscovered(args);
            }
        }

        public override void OnScanFailed(ScanFailure errorCode)
        {
            System.Diagnostics.Debug.WriteLine($"BLE scan failed: {errorCode}");
        }
    }
}

3. Register it in MauiProgram.cs

builder.Services.AddSingleton<IBleScanner, BleScanner>();

4. Use it from a page or view model

public partial class MainPage : ContentPage
{
    private readonly IBleScanner _bleScanner;

    public MainPage(IBleScanner bleScanner)
    {
        InitializeComponent();
        _bleScanner = bleScanner;
        _bleScanner.DeviceDiscovered += OnDeviceDiscovered;
    }

    private async void OnStartScanClicked(object sender, EventArgs e)
    {
        await _bleScanner.StartScanningAsync();
    }

    private async void OnStopScanClicked(object sender, EventArgs e)
    {
        await _bleScanner.StopScanningAsync();
    }

    private void OnDeviceDiscovered(object? sender, BleDeviceDiscoveredEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine(
            $"Found {e.Name ?? "(unknown)"} [{e.Id}] RSSI={e.Rssi}");
    }
}

5. Android permissions

Modern Android BLE scanning requires Bluetooth scan permissions, and older Android versions may also require location permission for scan results to work correctly.

In Platforms/Android/AndroidManifest.xml you will usually need:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

And on Android 12+, you also need to request the runtime permissions before scanning.

Side note

LowLatency is the most aggressive scan mode Android exposes, but it still does not guarantee perfectly continuous callbacks because behavior can vary by Android version, device, and power policy. BluetoothLeScanner is the correct Android API to use for scan operations, and ScanSettings is where you configure the scan mode.

Comments

Leave a comment below, or by email.

Last modified on 2026-02-28

comments powered by Disqus