Background location tracking

This commit is contained in:
2026-03-09 22:33:35 +08:00
parent 1e1c7d9552
commit ccc1c62262
10 changed files with 371 additions and 127 deletions
+171 -61
View File
@@ -1,78 +1,188 @@
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:workmanager/workmanager.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// Unique task name for the background location update.
const _taskName = 'com.tasq.backgroundLocationUpdate';
/// Interval between background location updates.
const _updateInterval = Duration(minutes: 15);
/// Top-level callback required by Workmanager. Must be a top-level or static
/// function.
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
/// 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 {
// Re-initialize Supabase in the isolate
await dotenv.load();
final url = dotenv.env['SUPABASE_URL'] ?? '';
final anonKey = dotenv.env['SUPABASE_ANON_KEY'] ?? '';
if (url.isEmpty || anonKey.isEmpty) return Future.value(true);
await Supabase.initialize(url: url, anonKey: anonKey);
final client = Supabase.instance.client;
// Must have an active session
final session = client.auth.currentSession;
if (session == null) return Future.value(true);
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return Future.value(true);
final permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
return Future.value(true);
}
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},
);
} catch (e) {
debugPrint('Background location update error: $e');
return Supabase.instance.client;
} catch (_) {
_bgIsolateInitialised = false;
}
return Future.value(true);
}
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();
});
}
/// Initialize Workmanager and register periodic background location task.
Future<void> initBackgroundLocationService() async {
if (kIsWeb) return; // Workmanager is not supported on web.
await Workmanager().initialize(callbackDispatcher);
/// 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;
}
/// Register a periodic task to report location every ~15 minutes
/// (Android minimum for periodic Workmanager tasks).
Future<void> startBackgroundLocationUpdates() async {
if (kIsWeb) return; // Workmanager is not supported on web.
await Workmanager().registerPeriodicTask(
_taskName,
_taskName,
frequency: const Duration(minutes: 15),
constraints: Constraints(networkType: NetworkType.connected),
existingWorkPolicy: ExistingPeriodicWorkPolicy.keep,
// ---------------------------------------------------------------------------
// 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],
),
);
}
/// Cancel the periodic background location task.
Future<void> stopBackgroundLocationUpdates() async {
if (kIsWeb) return; // Workmanager is not supported on web.
await Workmanager().cancelByUniqueName(_taskName);
/// 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();
}