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();