Replace paid flutter_background_geolocation with free flutter_foreground_task

Replace proprietary flutter_background_geolocation (requires paid license
for Android release builds) with free flutter_foreground_task package.
Background location tracking now uses foreground service with periodic
geolocation updates every 30 seconds.
This commit is contained in:
dmit.b
2026-06-25 13:24:24 +03:00
parent 5f59e17da8
commit 506608c508
6 changed files with 105 additions and 75 deletions
+17 -4
View File
@@ -8,17 +8,30 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application <application
android:label="family_safety_frontend" android:label="family_safety_frontend"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<service <service
android:name="com.transistorsoft.flutter.backgroundgeolocation.HeadlessJobService" android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" android:foregroundServiceType="location"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:exported="false" />
<receiver
android:name="com.pravera.flutter_foreground_task.receiver.AlarmReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.pravera.flutter_foreground_task.action.ALARM" />
</intent-filter>
</receiver>
<receiver
android:name="com.pravera.flutter_foreground_task.receiver.BootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
+2
View File
@@ -10,6 +10,7 @@ import '../providers/map_provider.dart';
import '../providers/share_provider.dart'; import '../providers/share_provider.dart';
import '../services/geo_service.dart'; import '../services/geo_service.dart';
import '../services/background_geo_service.dart'; import '../services/background_geo_service.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
class MapScreen extends StatefulWidget { class MapScreen extends StatefulWidget {
const MapScreen({super.key}); const MapScreen({super.key});
@@ -77,6 +78,7 @@ class _MapScreenState extends State<MapScreen> {
token: authProvider.token, token: authProvider.token,
geoId: shareProvider.geoId, geoId: shareProvider.geoId,
); );
FlutterForegroundTask.setTaskHandler(BackgroundTaskHandler());
await bgGeo.start(); await bgGeo.start();
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
+80 -56
View File
@@ -1,8 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import '../config/api.dart'; import '../config/api.dart';
@@ -19,11 +19,10 @@ class BackgroundGeoService {
String? _token; String? _token;
String? _geoId; String? _geoId;
bool _isReady = false; bool _isReady = false;
bool _isStarted = false;
bool _notificationsInitialized = false; bool _notificationsInitialized = false;
bool get isReady => _isReady; bool get isReady => _isReady;
bool get isStarted => _isStarted; Future<bool> get isStarted async => await FlutterForegroundTask.isRunningService;
String? get token => _token; String? get token => _token;
String? get geoId => _geoId; String? get geoId => _geoId;
@@ -33,11 +32,7 @@ class BackgroundGeoService {
const androidSettings = AndroidInitializationSettings( const androidSettings = AndroidInitializationSettings(
'@mipmap/ic_launcher', '@mipmap/ic_launcher',
); );
const iosSettings = DarwinInitializationSettings( const iosSettings = DarwinInitializationSettings();
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings( const initSettings = InitializationSettings(
android: androidSettings, android: androidSettings,
iOS: iosSettings, iOS: iosSettings,
@@ -61,69 +56,59 @@ class BackgroundGeoService {
await initNotifications(); await initNotifications();
bg.BackgroundGeolocation.onLocation(_onLocation, _onLocationError); FlutterForegroundTask.initCommunicationPort();
bg.BackgroundGeolocation.onMotionChange(_onMotionChange);
bg.BackgroundGeolocation.onProviderChange(_onProviderChange);
bg.BackgroundGeolocation.onHttp(_onHttp);
await bg.BackgroundGeolocation.ready(bg.Config( FlutterForegroundTask.addTaskDataCallback(_onTaskData);
desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
distanceFilter: 10.0,
stopOnTerminate: false,
startOnBoot: true,
debug: false,
logLevel: bg.Config.LOG_LEVEL_OFF,
autoSync: false,
));
_isReady = true; _isReady = true;
} }
Future<void> start() async { Future<void> start() async {
if (!_isReady) return; if (!_isReady) return;
if (_isStarted) return;
if (_token == null || _geoId == null || _geoId!.isEmpty) return; if (_token == null || _geoId == null || _geoId!.isEmpty) return;
await bg.BackgroundGeolocation.start(); final androidOptions = AndroidNotificationOptions(
_isStarted = true; channelId: 'background_geo_channel',
channelName: 'Background Geolocation',
channelDescription: 'Отслеживание позиции в фоновом режиме',
channelImportance: NotificationChannelImportance.HIGH,
priority: NotificationPriority.HIGH,
);
final iosOptions = IOSNotificationOptions(
showNotification: true,
playSound: true,
);
final taskOptions = ForegroundTaskOptions(
eventAction: ForegroundTaskEventAction.repeat(15000),
autoRunOnBoot: true,
allowWakeLock: true,
);
FlutterForegroundTask.init(
androidNotificationOptions: androidOptions,
iosNotificationOptions: iosOptions,
foregroundTaskOptions: taskOptions,
);
await FlutterForegroundTask.startService(
notificationTitle: 'Family Safety',
notificationText: 'Отслеживание позиции активно',
);
} }
Future<void> stop() async { Future<void> stop() async {
if (!_isStarted) return; await FlutterForegroundTask.stopService();
await bg.BackgroundGeolocation.stop();
_isStarted = false;
} }
void dispose() { void dispose() {
bg.BackgroundGeolocation.removeListeners();
_isReady = false; _isReady = false;
_isStarted = false;
_token = null; _token = null;
_geoId = null; _geoId = null;
} }
void _onLocation(bg.Location location) { static void _onTaskData(Object data) {}
if (_token == null || _geoId == null || _geoId!.isEmpty) return;
if (location.sample == true) return;
_sendPosition(location.coords.latitude, location.coords.longitude);
}
void _onLocationError(bg.LocationError error) {
_showErrorNotification('Ошибка геолокации: $error');
}
void _onMotionChange(bg.Location location) {
kDebugMode? print('[BackgroundGeo] Motion changed: isMoving=${location.isMoving}'):{};
}
void _onProviderChange(bg.ProviderChangeEvent event) {
kDebugMode?print('[BackgroundGeo] Provider changed: $event'):{};
}
void _onHttp(bg.HttpEvent event) {
kDebugMode? print('[BackgroundGeo] HTTP: status=${event.status}, success=${event.success}'):{};
}
Future<void> _sendPosition(double latitude, double longitude) async { Future<void> _sendPosition(double latitude, double longitude) async {
if (_token == null || _geoId == null || _geoId!.isEmpty) return; if (_token == null || _geoId == null || _geoId!.isEmpty) return;
@@ -162,10 +147,7 @@ class BackgroundGeoService {
priority: Priority.high, priority: Priority.high,
ticker: 'geo_error', ticker: 'geo_error',
); );
const iosDetails = DarwinNotificationDetails( const iosDetails = DarwinNotificationDetails();
presentAlert: true,
sound: 'default',
);
const details = NotificationDetails( const details = NotificationDetails(
android: androidDetails, android: androidDetails,
iOS: iosDetails, iOS: iosDetails,
@@ -184,3 +166,45 @@ class BackgroundGeoService {
print('[Notification] ${response.payload}'); print('[Notification] ${response.payload}');
} }
} }
class BackgroundTaskHandler extends TaskHandler {
int _locationCount = 0;
@override
Future<void> onStart(DateTime timestamp, TaskStarter starter) async {}
@override
void onRepeatEvent(DateTime timestamp) {
_locationCount++;
if (_locationCount % 2 != 0) return;
_getLocationAndSend();
}
@override
void onReceiveData(Object data) {}
@override
void onNotificationButtonPressed(String id) {}
@override
void onNotificationPressed() {}
@override
void onNotificationDismissed() {}
@override
Future<void> onDestroy(DateTime timestamp) async {}
Future<void> _getLocationAndSend() async {
try {
final position = await Geolocator.getCurrentPosition();
final bgGeo = BackgroundGeoService();
await bgGeo._sendPosition(position.latitude, position.longitude);
} catch (e) {
print('[BackgroundGeo] Error getting location: $e');
}
}
}
+5 -13
View File
@@ -17,14 +17,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.13.1" version: "2.13.1"
background_fetch:
dependency: transitive
description:
name: background_fetch
sha256: a185b471f6ff93c0074e2da98fbdf1e0c5af5a83adb97dfe924a9ef06c9e0175
url: "https://pub.dev"
source: hosted
version: "1.7.0"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -118,14 +110,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_background_geolocation: flutter_foreground_task:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_background_geolocation name: flutter_foreground_task
sha256: "2c5f23bf35837b31c4e1badadf7fb5da418a1862133b5a671b3e0bfa60c3bf2f" sha256: "206017ee1bf864f34b8d7bce664a172717caa21af8da23f55866470dfe316644"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.18.3" version: "8.17.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -668,4 +660,4 @@ packages:
version: "6.6.1" version: "6.6.1"
sdks: sdks:
dart: ">=3.10.1 <4.0.0" dart: ">=3.10.1 <4.0.0"
flutter: ">=3.38.0" flutter: ">=3.35.0"
+1 -1
View File
@@ -42,7 +42,7 @@ dependencies:
latlong2: ^0.9.1 latlong2: ^0.9.1
geolocator: ^14.0.2 geolocator: ^14.0.2
shared_preferences: ^2.2.2 shared_preferences: ^2.2.2
flutter_background_geolocation: ^4.13.0 flutter_foreground_task: ^8.0.0
flutter_local_notifications: ^18.0.1 flutter_local_notifications: ^18.0.1
dev_dependencies: dev_dependencies:
-1
View File
@@ -16,7 +16,6 @@ void main() {
testWidgets('App loads login screen', (WidgetTester tester) async { testWidgets('App loads login screen', (WidgetTester tester) async {
final bgGeo = BackgroundGeoService(); final bgGeo = BackgroundGeoService();
await bgGeo.init(); await bgGeo.init();
await bgGeo.initNotifications();
await tester.pumpWidget(MyApp( await tester.pumpWidget(MyApp(
settingsService: SettingsService(), settingsService: SettingsService(),
bgGeo: bgGeo, bgGeo: bgGeo,