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:
@@ -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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user