payfrit-user/lib/services/beacon_channel.dart
John Mizerek bc81ca98b0 Add native Kotlin beacon scanner for faster detection
- Add AltBeacon library for native Android beacon scanning
- Create BeaconScanner.kt with 2-second scan duration
- Set up MethodChannel bridge in MainActivity.kt
- Add beacon_channel.dart for Dart/native communication
- Update splash_screen.dart to use native scanner on Android
- Keep dchs_flutter_beacon for iOS compatibility

Native scanner eliminates Flutter plugin warmup overhead.
Beacons broadcast at 200ms, so 2s scan captures ~10 samples.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 00:53:46 -08:00

109 lines
2.9 KiB
Dart

import "dart:io";
import "package:flutter/services.dart";
/// Detected beacon from native scanner
class DetectedBeacon {
final String uuid;
final int rssi;
final int samples;
const DetectedBeacon({
required this.uuid,
required this.rssi,
required this.samples,
});
factory DetectedBeacon.fromMap(Map<dynamic, dynamic> map) {
return DetectedBeacon(
uuid: (map["uuid"] as String?) ?? "",
rssi: (map["rssi"] as int?) ?? -100,
samples: (map["samples"] as int?) ?? 0,
);
}
@override
String toString() => "DetectedBeacon(uuid: $uuid, rssi: $rssi, samples: $samples)";
}
/// Native beacon scanner via MethodChannel
/// Only works on Android. iOS falls back to Flutter plugin.
class BeaconChannel {
static const _channel = MethodChannel("com.payfrit.app/beacon");
/// Check if running on Android (native scanner only works there)
static bool get isSupported => Platform.isAndroid;
/// Check if Bluetooth permissions are granted
static Future<bool> hasPermissions() async {
if (!isSupported) return false;
try {
final result = await _channel.invokeMethod<bool>("hasPermissions");
return result ?? false;
} on PlatformException catch (e) {
print("[BeaconChannel] hasPermissions error: $e");
return false;
}
}
/// Check if Bluetooth is enabled
static Future<bool> isBluetoothEnabled() async {
if (!isSupported) return false;
try {
final result = await _channel.invokeMethod<bool>("isBluetoothEnabled");
return result ?? false;
} on PlatformException catch (e) {
print("[BeaconChannel] isBluetoothEnabled error: $e");
return false;
}
}
/// Start scanning for beacons
/// Returns list of detected beacons sorted by RSSI (strongest first)
static Future<List<DetectedBeacon>> startScan({List<String>? regions}) async {
if (!isSupported) {
print("[BeaconChannel] Not supported on this platform");
return [];
}
try {
print("[BeaconChannel] Starting native beacon scan...");
final result = await _channel.invokeMethod<List<dynamic>>(
"startScan",
{"regions": regions ?? []},
);
if (result == null) {
print("[BeaconChannel] Scan returned null");
return [];
}
final beacons = result
.map((e) => DetectedBeacon.fromMap(e as Map<dynamic, dynamic>))
.toList();
print("[BeaconChannel] Scan complete: found ${beacons.length} beacons");
for (final b in beacons) {
print("[BeaconChannel] $b");
}
return beacons;
} on PlatformException catch (e) {
print("[BeaconChannel] Scan error: ${e.message}");
return [];
}
}
/// Stop an ongoing scan
static Future<void> stopScan() async {
if (!isSupported) return;
try {
await _channel.invokeMethod("stopScan");
} on PlatformException catch (e) {
print("[BeaconChannel] stopScan error: $e");
}
}
}