using Plugin.BLE;
using Plugin.BLE.Abstractions.Contracts;
using Plugin.BLE.Abstractions.EventArgs;
namespace FieldLogger.Services.Ble;
/// A device found during a BLE scan.
public sealed record DiscoveredDevice(Guid Id, string Name, int Rssi);
/// Thin scan wrapper over Plugin.BLE with per-platform permission handling.
public sealed class BleScanner
{
private readonly IAdapter _adapter = CrossBluetoothLE.Current.Adapter;
public event EventHandler? DeviceDiscovered;
public bool IsScanning => _adapter.IsScanning;
///
/// Scans for BLE devices for the given duration, raising DeviceDiscovered as devices appear.
/// Only named devices are reported.
///
public async Task ScanAsync(TimeSpan duration, CancellationToken cancellationToken = default)
{
await EnsurePermissionsAsync();
if (!CrossBluetoothLE.Current.IsOn)
throw new InvalidOperationException("Bluetooth is turned off.");
void OnDiscovered(object? sender, DeviceEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Device.Name))
DeviceDiscovered?.Invoke(this, new DiscoveredDevice(e.Device.Id, e.Device.Name, e.Device.Rssi));
}
_adapter.DeviceDiscovered += OnDiscovered;
_adapter.DeviceAdvertised += OnDiscovered;
try
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(duration);
_adapter.ScanTimeout = (int)duration.TotalMilliseconds;
try
{
await _adapter.StartScanningForDevicesAsync(cancellationToken: timeout.Token);
}
catch (OperationCanceledException)
{
// Scan window elapsed or caller cancelled - both are normal.
}
}
finally
{
_adapter.DeviceDiscovered -= OnDiscovered;
_adapter.DeviceAdvertised -= OnDiscovered;
if (_adapter.IsScanning)
await _adapter.StopScanningForDevicesAsync();
}
}
/// Requests the runtime permissions BLE scanning needs (Android only; no-op elsewhere).
public static async Task EnsurePermissionsAsync()
{
#if ANDROID
if (OperatingSystem.IsAndroidVersionAtLeast(31))
{
var status = await Permissions.RequestAsync();
if (status != PermissionStatus.Granted)
throw new PermissionException("Bluetooth permission was denied.");
}
else
{
var status = await Permissions.RequestAsync();
if (status != PermissionStatus.Granted)
throw new PermissionException("Location permission (required for BLE scanning) was denied.");
}
#else
await Task.CompletedTask;
#endif
}
}