Add Android background geolocation with notification error handling

Add flutter_background_geolocation for background location tracking on Android.
Service automatically sends coordinates to server when app is in background.
Error messages are shown as system notifications using flutter_local_notifications.
This commit is contained in:
dmit.b
2026-06-25 13:20:39 +03:00
parent 86e9b5a22a
commit 5f59e17da8
8 changed files with 281 additions and 13 deletions
+11 -2
View File
@@ -6,6 +6,7 @@ import 'providers/map_provider.dart';
import 'providers/share_provider.dart';
import 'screens/login_screen.dart';
import 'screens/map_screen.dart';
import 'services/background_geo_service.dart';
import 'services/settings_service.dart';
void main() async {
@@ -13,13 +14,20 @@ void main() async {
final settings = SettingsService();
await settings.initialize();
ApiConfig.setBaseUrl(settings.baseUrl);
runApp(MyApp(settingsService: settings));
final bgGeo = BackgroundGeoService();
await bgGeo.init();
runApp(MyApp(settingsService: settings, bgGeo: bgGeo));
}
class MyApp extends StatelessWidget {
final SettingsService settingsService;
final BackgroundGeoService bgGeo;
const MyApp({super.key, required this.settingsService});
const MyApp({
super.key,
required this.settingsService,
required this.bgGeo,
});
@override
Widget build(BuildContext context) {
@@ -29,6 +37,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => MapProvider()),
ChangeNotifierProvider(create: (_) => ShareProvider()),
Provider.value(value: bgGeo),
],
child: MaterialApp(
title: 'Family Safety',
+13 -1
View File
@@ -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<MapScreen> {
position.longitude,
position.latitude,
);
if (!mounted) return;
final bgGeo = context.read<BackgroundGeoService>();
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<MapScreen> {
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
onPressed: () async {
_geoTimer?.cancel();
final bgGeo = context.read<BackgroundGeoService>();
await bgGeo.stop();
bgGeo.dispose();
authProvider.logout();
Navigator.of(context).pushReplacementNamed('/');
},
+186
View File
@@ -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<void> 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<void> 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<void> start() async {
if (!_isReady) return;
if (_isStarted) return;
if (_token == null || _geoId == null || _geoId!.isEmpty) return;
await bg.BackgroundGeolocation.start();
_isStarted = true;
}
Future<void> 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<void> _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<void> _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}');
}
}