189 lines
6.0 KiB
Dart
189 lines
6.0 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/widgets.dart';
|
||
import 'package:flutter_background_service/flutter_background_service.dart';
|
||
import 'package:geolocator/geolocator.dart';
|
||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||
|
||
/// Interval between background location updates.
|
||
const _updateInterval = Duration(minutes: 15);
|
||
|
||
/// Whether the Supabase client has been initialised inside the background
|
||
/// isolate. We guard against double-init which would throw.
|
||
bool _bgIsolateInitialised = false;
|
||
|
||
/// Initialise Supabase inside the background isolate so we can call RPCs.
|
||
/// Safe to call multiple times – only the first call performs real work.
|
||
Future<SupabaseClient?> _ensureBgSupabase() async {
|
||
if (_bgIsolateInitialised) {
|
||
try {
|
||
return Supabase.instance.client;
|
||
} catch (_) {
|
||
_bgIsolateInitialised = false;
|
||
}
|
||
}
|
||
|
||
try {
|
||
await dotenv.load();
|
||
} catch (_) {
|
||
// .env may already be loaded
|
||
}
|
||
|
||
final url = dotenv.env['SUPABASE_URL'] ?? '';
|
||
final anonKey = dotenv.env['SUPABASE_ANON_KEY'] ?? '';
|
||
if (url.isEmpty || anonKey.isEmpty) return null;
|
||
|
||
try {
|
||
await Supabase.initialize(url: url, anonKey: anonKey);
|
||
} catch (_) {
|
||
// Already initialised – that's fine
|
||
}
|
||
_bgIsolateInitialised = true;
|
||
return Supabase.instance.client;
|
||
}
|
||
|
||
/// Send location update to Supabase via the `update_live_position` RPC.
|
||
Future<void> _sendLocationUpdate() async {
|
||
try {
|
||
final client = await _ensureBgSupabase();
|
||
if (client == null) {
|
||
debugPrint('[BgLoc] Supabase client unavailable');
|
||
return;
|
||
}
|
||
|
||
final session = client.auth.currentSession;
|
||
if (session == null) {
|
||
debugPrint('[BgLoc] No active session');
|
||
return;
|
||
}
|
||
|
||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||
if (!serviceEnabled) {
|
||
debugPrint('[BgLoc] Location service disabled');
|
||
return;
|
||
}
|
||
|
||
final permission = await Geolocator.checkPermission();
|
||
if (permission == LocationPermission.denied ||
|
||
permission == LocationPermission.deniedForever) {
|
||
debugPrint('[BgLoc] Location permission denied');
|
||
return;
|
||
}
|
||
|
||
final position = await Geolocator.getCurrentPosition(
|
||
locationSettings: const LocationSettings(accuracy: LocationAccuracy.high),
|
||
);
|
||
|
||
await client.rpc(
|
||
'update_live_position',
|
||
params: {'p_lat': position.latitude, 'p_lng': position.longitude},
|
||
);
|
||
debugPrint(
|
||
'[BgLoc] Position updated: ${position.latitude}, ${position.longitude}',
|
||
);
|
||
} catch (e) {
|
||
debugPrint('[BgLoc] Error: $e');
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Background service entry point (runs in a separate isolate on Android).
|
||
// MUST be a PUBLIC top-level function — PluginUtilities.getCallbackHandle()
|
||
// returns null for private (underscore-prefixed) functions.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Top-level entry point invoked by flutter_background_service in a new isolate.
|
||
@pragma('vm:entry-point')
|
||
Future<void> onBackgroundServiceStart(ServiceInstance service) async {
|
||
// Required for platform channel access (Geolocator, Supabase) in an isolate.
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
debugPrint('[BgLoc] Background service started');
|
||
|
||
// Send initial location immediately on service start.
|
||
await _sendLocationUpdate();
|
||
|
||
// Periodic 15-minute timer for location updates.
|
||
Timer.periodic(_updateInterval, (_) async {
|
||
debugPrint('[BgLoc] Periodic update triggered');
|
||
await _sendLocationUpdate();
|
||
});
|
||
|
||
// Listen for stop command from the foreground.
|
||
service.on('stop').listen((_) {
|
||
debugPrint('[BgLoc] Stop command received');
|
||
service.stopSelf();
|
||
});
|
||
}
|
||
|
||
/// iOS background entry point — must be a PUBLIC top-level function.
|
||
@pragma('vm:entry-point')
|
||
Future<bool> onIosBackground(ServiceInstance service) async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
await _sendLocationUpdate();
|
||
return true;
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Public API – same function signatures as the previous implementation.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Initialise the background service plugin. Call once at app startup.
|
||
Future<void> initBackgroundLocationService() async {
|
||
if (kIsWeb) return;
|
||
|
||
final service = FlutterBackgroundService();
|
||
await service.configure(
|
||
iosConfiguration: IosConfiguration(
|
||
autoStart: false,
|
||
onForeground: onBackgroundServiceStart,
|
||
onBackground: onIosBackground,
|
||
),
|
||
androidConfiguration: AndroidConfiguration(
|
||
onStart: onBackgroundServiceStart,
|
||
isForegroundMode: true,
|
||
autoStart: false,
|
||
autoStartOnBoot: true,
|
||
// Leave notificationChannelId null so the plugin will create the
|
||
// default "FOREGROUND_DEFAULT" channel for us. Passing a non-null
|
||
// value requires us to create the channel ourselves first, otherwise
|
||
// the service crashes on API >=26 when startForeground is called.
|
||
notificationChannelId: null,
|
||
initialNotificationTitle: 'TasQ',
|
||
initialNotificationContent: 'Location tracking is active',
|
||
foregroundServiceNotificationId: 7749,
|
||
foregroundServiceTypes: [AndroidForegroundType.location],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Start the background location service.
|
||
Future<void> startBackgroundLocationUpdates() async {
|
||
if (kIsWeb) return;
|
||
|
||
final service = FlutterBackgroundService();
|
||
final running = await service.isRunning();
|
||
if (running) return;
|
||
|
||
await service.startService();
|
||
}
|
||
|
||
/// Stop the background location service.
|
||
Future<void> stopBackgroundLocationUpdates() async {
|
||
if (kIsWeb) return;
|
||
|
||
final service = FlutterBackgroundService();
|
||
final running = await service.isRunning();
|
||
if (!running) return;
|
||
|
||
service.invoke('stop');
|
||
}
|
||
|
||
/// Check whether the background location service is currently running.
|
||
Future<bool> isBackgroundLocationRunning() async {
|
||
if (kIsWeb) return false;
|
||
return FlutterBackgroundService().isRunning();
|
||
}
|