tasq/lib/services/background_location_service.dart

189 lines
6.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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&nbsp;>=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();
}