diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 59cc5c8..2652ad7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,11 +2,23 @@
+
+
+
+
+
+
+
AuthProvider()),
ChangeNotifierProvider(create: (_) => MapProvider()),
ChangeNotifierProvider(create: (_) => ShareProvider()),
+ Provider.value(value: bgGeo),
],
child: MaterialApp(
title: 'Family Safety',
diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart
index f789542..2a4a9dd 100644
--- a/lib/screens/map_screen.dart
+++ b/lib/screens/map_screen.dart
@@ -9,6 +9,7 @@ import '../providers/auth_provider.dart';
import '../providers/map_provider.dart';
import '../providers/share_provider.dart';
import '../services/geo_service.dart';
+import '../services/background_geo_service.dart';
class MapScreen extends StatefulWidget {
const MapScreen({super.key});
@@ -69,6 +70,14 @@ class _MapScreenState extends State {
position.longitude,
position.latitude,
);
+
+ if (!mounted) return;
+ final bgGeo = context.read();
+ bgGeo.configure(
+ token: authProvider.token,
+ geoId: shareProvider.geoId,
+ );
+ await bgGeo.start();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(
@@ -219,8 +228,11 @@ class _MapScreenState extends State {
),
IconButton(
icon: const Icon(Icons.logout),
- onPressed: () {
+ onPressed: () async {
_geoTimer?.cancel();
+ final bgGeo = context.read();
+ await bgGeo.stop();
+ bgGeo.dispose();
authProvider.logout();
Navigator.of(context).pushReplacementNamed('/');
},
diff --git a/lib/services/background_geo_service.dart b/lib/services/background_geo_service.dart
new file mode 100644
index 0000000..04c4b44
--- /dev/null
+++ b/lib/services/background_geo_service.dart
@@ -0,0 +1,186 @@
+import 'dart:async';
+import 'dart:convert';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+import 'package:http/http.dart' as http;
+import '../config/api.dart';
+
+class BackgroundGeoService {
+ static final BackgroundGeoService _instance = BackgroundGeoService._init();
+
+ factory BackgroundGeoService() => _instance;
+
+ BackgroundGeoService._init();
+
+ final http.Client _client = http.Client();
+ final FlutterLocalNotificationsPlugin _notifications =
+ FlutterLocalNotificationsPlugin();
+ String? _token;
+ String? _geoId;
+ bool _isReady = false;
+ bool _isStarted = false;
+ bool _notificationsInitialized = false;
+
+ bool get isReady => _isReady;
+ bool get isStarted => _isStarted;
+ String? get token => _token;
+ String? get geoId => _geoId;
+
+ Future initNotifications() async {
+ if (_notificationsInitialized) return;
+
+ const androidSettings = AndroidInitializationSettings(
+ '@mipmap/ic_launcher',
+ );
+ const iosSettings = DarwinInitializationSettings(
+ requestAlertPermission: true,
+ requestBadgePermission: true,
+ requestSoundPermission: true,
+ );
+ const initSettings = InitializationSettings(
+ android: androidSettings,
+ iOS: iosSettings,
+ );
+
+ await _notifications.initialize(
+ initSettings,
+ onDidReceiveNotificationResponse: _onNotificationResponse,
+ );
+
+ _notificationsInitialized = true;
+ }
+
+ void configure({required String token, required String geoId}) {
+ _token = token;
+ _geoId = geoId;
+ }
+
+ Future init() async {
+ if (_isReady) return;
+
+ await initNotifications();
+
+ bg.BackgroundGeolocation.onLocation(_onLocation, _onLocationError);
+ bg.BackgroundGeolocation.onMotionChange(_onMotionChange);
+ bg.BackgroundGeolocation.onProviderChange(_onProviderChange);
+ bg.BackgroundGeolocation.onHttp(_onHttp);
+
+ await bg.BackgroundGeolocation.ready(bg.Config(
+ 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;
+ }
+
+ Future start() async {
+ if (!_isReady) return;
+ if (_isStarted) return;
+ if (_token == null || _geoId == null || _geoId!.isEmpty) return;
+
+ await bg.BackgroundGeolocation.start();
+ _isStarted = true;
+ }
+
+ Future stop() async {
+ if (!_isStarted) return;
+ await bg.BackgroundGeolocation.stop();
+ _isStarted = false;
+ }
+
+ void dispose() {
+ bg.BackgroundGeolocation.removeListeners();
+ _isReady = false;
+ _isStarted = false;
+ _token = null;
+ _geoId = null;
+ }
+
+ void _onLocation(bg.Location location) {
+ 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 _sendPosition(double latitude, double longitude) async {
+ if (_token == null || _geoId == null || _geoId!.isEmpty) return;
+
+ try {
+ final response = await _client.put(
+ Uri.parse('${ApiConfig.geoUrl}?id=$_geoId'),
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer $_token',
+ },
+ body: jsonEncode({
+ 'x': longitude,
+ 'y': latitude,
+ }),
+ );
+
+ if (response.statusCode != 200) {
+ _showErrorNotification(
+ 'Не удалось отправить позицию. Код ответа: ${response.statusCode}',
+ );
+ }
+ } catch (e) {
+ _showErrorNotification('Ошибка отправки позиции: $e');
+ }
+ }
+
+ Future _showErrorNotification(String message) async {
+ if (!_notificationsInitialized) return;
+
+ const androidDetails = AndroidNotificationDetails(
+ 'background_geo_errors',
+ 'Background Geo Errors',
+ channelDescription: 'Уведомления об ошибках геолокации в фоне',
+ importance: Importance.high,
+ priority: Priority.high,
+ ticker: 'geo_error',
+ );
+ const iosDetails = DarwinNotificationDetails(
+ presentAlert: true,
+ sound: 'default',
+ );
+ const details = NotificationDetails(
+ android: androidDetails,
+ iOS: iosDetails,
+ );
+
+ await _notifications.show(
+ 0,
+ 'Ошибка геолокации',
+ message,
+ details,
+ payload: 'geo_error',
+ );
+ }
+
+ void _onNotificationResponse(NotificationResponse response) {
+ print('[Notification] ${response.payload}');
+ }
+}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 8679ef3..5f8549d 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,11 +5,13 @@
import FlutterMacOS
import Foundation
+import flutter_local_notifications
import geolocator_apple
import package_info_plus
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index b7d2b60..2703b3e 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -49,16 +57,8 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
- convert:
- dependency: "direct main"
- description:
- name: convert
- sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
- url: "https://pub.dev"
- source: hosted
- version: "3.1.2"
crypto:
- dependency: "direct main"
+ dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -118,6 +118,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_background_geolocation:
+ dependency: "direct main"
+ description:
+ name: flutter_background_geolocation
+ sha256: "2c5f23bf35837b31c4e1badadf7fb5da418a1862133b5a671b3e0bfa60c3bf2f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.18.3"
flutter_lints:
dependency: "direct dev"
description:
@@ -126,6 +134,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_local_notifications:
+ dependency: "direct main"
+ description:
+ name: flutter_local_notifications
+ sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
+ url: "https://pub.dev"
+ source: hosted
+ version: "18.0.1"
+ flutter_local_notifications_linux:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_linux
+ sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ flutter_local_notifications_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_platform_interface
+ sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -546,6 +578,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.7"
+ timezone:
+ dependency: transitive
+ description:
+ name: timezone
+ sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.1"
typed_data:
dependency: transitive
description:
@@ -628,4 +668,4 @@ packages:
version: "6.6.1"
sdks:
dart: ">=3.10.1 <4.0.0"
- flutter: ">=3.35.0"
+ flutter: ">=3.38.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index 1736805..57ae472 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -42,6 +42,8 @@ dependencies:
latlong2: ^0.9.1
geolocator: ^14.0.2
shared_preferences: ^2.2.2
+ flutter_background_geolocation: ^4.13.0
+ flutter_local_notifications: ^18.0.1
dev_dependencies:
flutter_test:
diff --git a/test/widget_test.dart b/test/widget_test.dart
index 7d1b011..c2a4a2a 100644
--- a/test/widget_test.dart
+++ b/test/widget_test.dart
@@ -9,12 +9,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:family_safety_frontend/main.dart';
+import 'package:family_safety_frontend/services/background_geo_service.dart';
import 'package:family_safety_frontend/services/settings_service.dart';
void main() {
testWidgets('App loads login screen', (WidgetTester tester) async {
+ final bgGeo = BackgroundGeoService();
+ await bgGeo.init();
+ await bgGeo.initNotifications();
await tester.pumpWidget(MyApp(
settingsService: SettingsService(),
+ bgGeo: bgGeo,
));
await tester.pump();