diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index a2057a5..f34e154 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -2,7 +2,18 @@
"permissions": {
"allow": [
"Bash(flutter analyze:*)",
- "Bash(flutter devices:*)"
+ "Bash(flutter devices:*)",
+ "Bash(flutter build:*)",
+ "Bash(flutter install:*)",
+ "Bash(open:*)",
+ "Bash(xcrun devicectl device uninstall app:*)",
+ "Bash(pod repo update:*)",
+ "Bash(pod install)",
+ "Bash(flutter clean:*)",
+ "Bash(flutter pub get:*)",
+ "Bash(flutter run:*)",
+ "Bash(nc:*)",
+ "Bash(pkill:*)"
],
"deny": [],
"ask": []
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b6a48e6..c2ae9b8 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -8,6 +8,11 @@
+
+
+
+
+
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..2bc8e05
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,4 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
+ - provider: true
\ No newline at end of file
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 2d271d1..e4b96bd 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -34,6 +34,8 @@ PODS:
- DKImagePickerController/PhotoGallery
- Flutter
- Flutter (1.0.0)
+ - flutter_background_service_ios (0.0.3):
+ - Flutter
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
@@ -66,6 +68,7 @@ PODS:
DEPENDENCIES:
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
+ - flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
@@ -89,6 +92,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
+ flutter_background_service_ios:
+ :path: ".symlinks/plugins/flutter_background_service_ios/ios"
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_compass:
@@ -113,6 +118,7 @@ SPEC CHECKSUMS:
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index bd08024..9446723 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -105,7 +105,6 @@
2FF0A626EBAC324A9D2CD7C8 /* Pods-RunnerTests.release.xcconfig */,
11010AF1B9B58058CB45D836 /* Pods-RunnerTests.profile.xcconfig */,
);
- name = Pods;
path = Pods;
sourceTree = "";
};
@@ -454,6 +453,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -476,6 +476,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
@@ -489,7 +490,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = PM784W7B8X;
+ DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -578,6 +579,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -607,6 +609,7 @@
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -635,6 +638,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -657,6 +661,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -672,7 +677,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = PM784W7B8X;
+ DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -695,7 +700,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = PM784W7B8X;
+ DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024 1.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024 1.png
new file mode 100644
index 0000000..df9b639
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024 1.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024 2.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024 2.png
new file mode 100644
index 0000000..df9b639
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024 2.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024.png
new file mode 100644
index 0000000..df9b639
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/AppIcon 11024x1024.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index d36b1fa..d2ac72f 100644
--- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1,38 @@
{
"images" : [
{
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
+ "filename" : "AppIcon 11024x1024.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
},
{
- "size" : "20x20",
- "idiom" : "iphone",
- "filename" : "Icon-App-20x20@3x.png",
- "scale" : "3x"
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "filename" : "AppIcon 11024x1024 1.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
},
{
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "iphone",
- "filename" : "Icon-App-29x29@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "iphone",
- "filename" : "Icon-App-40x40@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "60x60",
- "idiom" : "iphone",
- "filename" : "Icon-App-60x60@3x.png",
- "scale" : "3x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "20x20",
- "idiom" : "ipad",
- "filename" : "Icon-App-20x20@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "29x29",
- "idiom" : "ipad",
- "filename" : "Icon-App-29x29@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "40x40",
- "idiom" : "ipad",
- "filename" : "Icon-App-40x40@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@1x.png",
- "scale" : "1x"
- },
- {
- "size" : "76x76",
- "idiom" : "ipad",
- "filename" : "Icon-App-76x76@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "83.5x83.5",
- "idiom" : "ipad",
- "filename" : "Icon-App-83.5x83.5@2x.png",
- "scale" : "2x"
- },
- {
- "size" : "1024x1024",
- "idiom" : "ios-marketing",
- "filename" : "Icon-App-1024x1024@1x.png",
- "scale" : "1x"
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "tinted"
+ }
+ ],
+ "filename" : "AppIcon 11024x1024 2.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
}
],
"info" : {
- "version" : 1,
- "author" : "xcode"
+ "author" : "xcode",
+ "version" : 1
}
}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
deleted file mode 100644
index dc9ada4..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
deleted file mode 100644
index 7353c41..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
deleted file mode 100644
index 6ed2d93..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
deleted file mode 100644
index 4cd7b00..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
deleted file mode 100644
index fe73094..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
deleted file mode 100644
index 321773c..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
deleted file mode 100644
index 797d452..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
deleted file mode 100644
index 502f463..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
deleted file mode 100644
index 0ec3034..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
deleted file mode 100644
index e9f5fea..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
deleted file mode 100644
index 84ac32a..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
deleted file mode 100644
index 8953cba..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
deleted file mode 100644
index 0467bf1..0000000
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ
diff --git a/ios/Runner/Assets.xcassets/Contents.json b/ios/Runner/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 5f014a8..28972d0 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -59,5 +59,14 @@
NSMotionUsageDescription
MeshCore SAR needs access to the compass to show your heading direction on the map
+ UIBackgroundModes
+
+ location
+ fetch
+
+ BGTaskSchedulerPermittedIdentifiers
+
+ dev.flutter.background.refresh
+
diff --git a/lib/providers/connection_provider.dart b/lib/providers/connection_provider.dart
index 121ee27..643e8a9 100644
--- a/lib/providers/connection_provider.dart
+++ b/lib/providers/connection_provider.dart
@@ -13,6 +13,9 @@ import '../utils/sar_message_parser.dart';
class ConnectionProvider with ChangeNotifier {
final MeshCoreBleService _bleService = MeshCoreBleService();
+ /// Expose BLE service for background location tracking
+ MeshCoreBleService get bleService => _bleService;
+
DeviceInfo _deviceInfo = DeviceInfo();
DeviceInfo get deviceInfo => _deviceInfo;
diff --git a/lib/screens/map_tab.dart b/lib/screens/map_tab.dart
index 855e613..7e00d04 100644
--- a/lib/screens/map_tab.dart
+++ b/lib/screens/map_tab.dart
@@ -16,6 +16,7 @@ import '../models/contact.dart';
import '../models/sar_marker.dart';
import '../models/map_layer.dart';
import '../services/tile_cache_service.dart';
+import '../services/background_location_service.dart';
import '../widgets/map_markers.dart';
import '../widgets/map_debug_info.dart';
import 'map_management_screen.dart';
@@ -31,6 +32,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
final MapController _mapController = MapController();
final TileCacheService _tileCache = TileCacheService();
bool _isInitialized = false;
+ bool _isMapReady = false; // Track when map widget is actually rendered
MapLayer _currentLayer = MapLayer.openStreetMap;
Position? _currentPosition;
double? _compassHeading; // Compass sensor heading
@@ -38,8 +40,10 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
bool _showLegend = false;
bool _showMapDebugInfo = false; // Toggle for debug info
double _gpsUpdateDistance = 3.0; // meters
+ bool _backgroundTrackingEnabled = false; // Toggle for background tracking
StreamSubscription? _positionStreamSubscription;
StreamSubscription? _compassStreamSubscription;
+ final BackgroundLocationService _backgroundLocationService = BackgroundLocationService();
// Saved map position (loaded from SharedPreferences)
LatLng? _savedMapCenter;
@@ -64,6 +68,13 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
WidgetsBinding.instance.addPostFrameCallback((_) {
final mapProvider = context.read();
mapProvider.addListener(_handleMapNavigation);
+
+ // Initialize background location service with BLE service
+ final appProvider = context.read();
+ _backgroundLocationService.initialize(appProvider.connectionProvider.bleService);
+
+ // Restore background tracking state
+ _restoreBackgroundTracking();
});
}
@@ -82,8 +93,8 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
});
// Rotate map if rotation mode is enabled and we have compass heading
- // Only rotate if map is initialized
- if (_rotateMarkerWithHeading && event.heading != null && _isInitialized) {
+ // Only rotate if map is ready
+ if (_rotateMarkerWithHeading && event.heading != null && _isMapReady) {
try {
// Use moveAndRotate to set absolute rotation
final camera = _mapController.camera;
@@ -117,6 +128,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
_rotateMarkerWithHeading = prefs.getBool('map_rotate_with_heading') ?? false;
_showMapDebugInfo = prefs.getBool('map_show_debug_info') ?? false;
_gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 3.0;
+ _backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false;
// Store saved position for use in build
if (lastLat != null && lastLon != null && lastZoom != null) {
@@ -138,15 +150,21 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
await prefs.setBool('map_rotate_with_heading', _rotateMarkerWithHeading);
await prefs.setBool('map_show_debug_info', _showMapDebugInfo);
await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
+ await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled);
await prefs.setInt('map_last_layer', MapLayer.allLayers.indexOf(_currentLayer));
}
Future _saveMapPosition() async {
- final prefs = await SharedPreferences.getInstance();
- final camera = _mapController.camera;
- await prefs.setDouble('map_last_latitude', camera.center.latitude);
- await prefs.setDouble('map_last_longitude', camera.center.longitude);
- await prefs.setDouble('map_last_zoom', camera.zoom);
+ if (!_isMapReady) return;
+ try {
+ final prefs = await SharedPreferences.getInstance();
+ final camera = _mapController.camera;
+ await prefs.setDouble('map_last_latitude', camera.center.latitude);
+ await prefs.setDouble('map_last_longitude', camera.center.longitude);
+ await prefs.setDouble('map_last_zoom', camera.zoom);
+ } catch (e) {
+ debugPrint('Error saving map position: $e');
+ }
}
Future _requestLocationPermission() async {
@@ -198,13 +216,17 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
// Rotate map if rotation mode is enabled and heading is available
// Heading of -1.0 means heading is unavailable
- if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
- final camera = _mapController.camera;
- _mapController.moveAndRotate(
- camera.center,
- camera.zoom,
- -position.heading,
- );
+ if (_isMapReady && _rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
+ try {
+ final camera = _mapController.camera;
+ _mapController.moveAndRotate(
+ camera.center,
+ camera.zoom,
+ -position.heading,
+ );
+ } catch (e) {
+ // Map not ready yet, ignore
+ }
}
}
});
@@ -212,13 +234,18 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
void _handleMapNavigation() {
final mapProvider = context.read();
- if (mapProvider.targetLocation != null && _isInitialized) {
- _mapController.move(
- mapProvider.targetLocation!,
- mapProvider.targetZoom ?? _defaultZoom,
- );
- // Clear the navigation request after handling
- mapProvider.clearNavigation();
+ if (mapProvider.targetLocation != null && _isMapReady) {
+ try {
+ _mapController.move(
+ mapProvider.targetLocation!,
+ mapProvider.targetZoom ?? _defaultZoom,
+ );
+ // Clear the navigation request after handling
+ mapProvider.clearNavigation();
+ } catch (e) {
+ // Map not ready yet, ignore
+ debugPrint('Map controller not ready for navigation: $e');
+ }
}
}
@@ -229,6 +256,20 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
setState(() {
_isInitialized = true;
});
+ // Wait for the map to render, then mark it as ready
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ // Give the map widget one more frame to fully initialize
+ Future.delayed(const Duration(milliseconds: 100), () {
+ if (mounted) {
+ setState(() {
+ _isMapReady = true;
+ });
+ debugPrint('Map is now ready for controller operations');
+ }
+ });
+ }
+ });
}
} catch (e) {
debugPrint('Error initializing tile cache: $e');
@@ -236,6 +277,19 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
setState(() {
_isInitialized = true; // Continue without caching
});
+ // Still mark map as ready after a delay
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ Future.delayed(const Duration(milliseconds: 100), () {
+ if (mounted) {
+ setState(() {
+ _isMapReady = true;
+ });
+ debugPrint('Map is now ready for controller operations');
+ }
+ });
+ }
+ });
}
}
}
@@ -267,6 +321,17 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
return null;
}
+ // Safely get map rotation, returns 0.0 if map is not ready
+ double _getMapRotation() {
+ if (!_isMapReady) return 0.0;
+ try {
+ return _mapController.camera.rotation;
+ } catch (e) {
+ // Map controller not ready yet
+ return 0.0;
+ }
+ }
+
LatLng _calculateCenter(List contacts, List sarMarkers) {
final allPoints = [];
@@ -346,23 +411,29 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
}
void _navigateToDownload(BuildContext context) {
- // Get current map bounds
- final bounds = _mapController.camera.visibleBounds;
- final currentZoom = _mapController.camera.zoom.round();
+ if (!_isMapReady) return;
- // Navigate to Map Management screen with pre-populated data
- final appProvider = context.read();
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => MapManagementScreen(
- tileCacheService: appProvider.tileCacheService,
- initialLayer: _currentLayer,
- initialBounds: bounds,
- initialZoom: currentZoom,
+ try {
+ // Get current map bounds
+ final bounds = _mapController.camera.visibleBounds;
+ final currentZoom = _mapController.camera.zoom.round();
+
+ // Navigate to Map Management screen with pre-populated data
+ final appProvider = context.read();
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => MapManagementScreen(
+ tileCacheService: appProvider.tileCacheService,
+ initialLayer: _currentLayer,
+ initialBounds: bounds,
+ initialZoom: currentZoom,
+ ),
),
- ),
- );
+ );
+ } catch (e) {
+ debugPrint('Error accessing map camera: $e');
+ }
}
void _showOptionsMenu(BuildContext context) {
@@ -416,17 +487,23 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
onChanged: (value) {
setState(() {
_rotateMarkerWithHeading = value;
- // Reset map rotation when disabling
- final camera = _mapController.camera;
- if (!_rotateMarkerWithHeading) {
- _mapController.moveAndRotate(camera.center, camera.zoom, 0);
- } else if (_currentHeading != null) {
- // Apply current heading rotation when enabling (if heading is valid)
- _mapController.moveAndRotate(
- camera.center,
- camera.zoom,
- -_currentHeading!,
- );
+ // Reset map rotation when disabling (only if map is ready)
+ if (_isMapReady) {
+ try {
+ final camera = _mapController.camera;
+ if (!_rotateMarkerWithHeading) {
+ _mapController.moveAndRotate(camera.center, camera.zoom, 0);
+ } else if (_currentHeading != null) {
+ // Apply current heading rotation when enabling (if heading is valid)
+ _mapController.moveAndRotate(
+ camera.center,
+ camera.zoom,
+ -_currentHeading!,
+ );
+ }
+ } catch (e) {
+ // Map not ready yet, ignore
+ }
}
});
setModalState(() {});
@@ -434,56 +511,6 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
},
),
const Divider(),
- // GPS Update Distance
- ListTile(
- leading: const Icon(Icons.gps_fixed),
- title: const Text('GPS Update Distance'),
- subtitle: Text('${_gpsUpdateDistance.toStringAsFixed(0)} meters'),
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Column(
- children: [
- Slider(
- value: _gpsUpdateDistance,
- min: 1,
- max: 20,
- divisions: 19,
- label: '${_gpsUpdateDistance.toStringAsFixed(0)}m',
- onChanged: (value) {
- setModalState(() {
- _gpsUpdateDistance = value;
- });
- },
- onChangeEnd: (value) {
- setState(() {
- _gpsUpdateDistance = value;
- });
- // Restart location stream with new distance
- _restartLocationStream();
- _saveSettings();
- },
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- '1m',
- style: Theme.of(context).textTheme.bodySmall,
- ),
- Text(
- '20m',
- style: Theme.of(context).textTheme.bodySmall,
- ),
- ],
- ),
- ),
- ],
- ),
- ),
- const Divider(),
// Map Debug Info toggle
SwitchListTile(
secondary: const Icon(Icons.developer_mode),
@@ -599,16 +626,60 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
// Rotate map if rotation mode is enabled and heading is available
// Heading of -1.0 means heading is unavailable
- if (_rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
- final camera = _mapController.camera;
- _mapController.moveAndRotate(
- camera.center,
- camera.zoom,
- -position.heading,
- );
+ if (_isMapReady && _rotateMarkerWithHeading && position.heading != null && position.heading >= 0) {
+ try {
+ final camera = _mapController.camera;
+ _mapController.moveAndRotate(
+ camera.center,
+ camera.zoom,
+ -position.heading,
+ );
+ } catch (e) {
+ // Map not ready yet, ignore
+ }
}
}
});
+
+ // Update background tracking distance if active
+ if (_backgroundTrackingEnabled) {
+ _backgroundLocationService.updateDistanceThreshold(_gpsUpdateDistance);
+ }
+ }
+
+ /// Restore background tracking state on app start
+ Future _restoreBackgroundTracking() async {
+ if (_backgroundTrackingEnabled) {
+ await _startBackgroundTracking();
+ }
+ }
+
+ /// Start background location tracking
+ Future _startBackgroundTracking() async {
+ final success = await _backgroundLocationService.startTracking(
+ distanceThreshold: _gpsUpdateDistance,
+ );
+
+ if (!success) {
+ if (mounted) {
+ setState(() {
+ _backgroundTrackingEnabled = false;
+ });
+ _saveSettings();
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Failed to start background tracking. Check permissions and BLE connection.'),
+ duration: Duration(seconds: 3),
+ ),
+ );
+ }
+ }
+ }
+
+ /// Stop background location tracking
+ Future _stopBackgroundTracking() async {
+ await _backgroundLocationService.stopTracking();
}
@override
@@ -654,7 +725,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
...MapMarkers.createTeamMemberMarkers(
contactsWithLocation,
context,
- mapRotation: _mapController.camera.rotation,
+ mapRotation: _getMapRotation(),
onContactTap: (contact) {
_showDetailedCompassWithContact(
context,
@@ -667,7 +738,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
...MapMarkers.createSarMarkers(
sarMarkers,
context,
- mapRotation: _mapController.camera.rotation,
+ mapRotation: _getMapRotation(),
onSarMarkerTap: (marker) {
_showDetailedCompassWithSarMarker(
context,
@@ -766,7 +837,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
children: [
FloatingActionButton.small(
heroTag: 'center_map',
- onPressed: () async {
+ onPressed: !_isMapReady ? null : () async {
// Force update GPS location and jump to it
try {
final position = await Geolocator.getCurrentPosition(
@@ -818,7 +889,7 @@ class _MapTabState extends State with AutomaticKeepAliveClientMixin {
),
),
// Map debug info - bottom left
- if (_showMapDebugInfo && _isInitialized)
+ if (_showMapDebugInfo && _isMapReady)
Positioned(
bottom: 16,
left: 16,
diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart
index cf7240c..54cfb89 100644
--- a/lib/screens/settings_screen.dart
+++ b/lib/screens/settings_screen.dart
@@ -6,6 +6,8 @@ import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import '../providers/contacts_provider.dart';
import '../providers/messages_provider.dart';
+import '../providers/app_provider.dart';
+import '../services/background_location_service.dart';
import '../utils/sample_data_generator.dart';
class SettingsScreen extends StatefulWidget {
@@ -26,12 +28,16 @@ class _SettingsScreenState extends State {
late ThemeMode _selectedTheme;
PackageInfo? _packageInfo;
bool _isLoadingSampleData = false;
+ double _gpsUpdateDistance = 10.0;
+ bool _backgroundTrackingEnabled = false;
+ final BackgroundLocationService _backgroundLocationService = BackgroundLocationService();
@override
void initState() {
super.initState();
_selectedTheme = widget.currentTheme;
_loadPackageInfo();
+ _loadLocationSettings();
}
Future _loadPackageInfo() async {
@@ -43,6 +49,35 @@ class _SettingsScreenState extends State {
}
}
+ Future _loadLocationSettings() async {
+ final prefs = await SharedPreferences.getInstance();
+ if (mounted) {
+ setState(() {
+ _gpsUpdateDistance = prefs.getDouble('map_gps_update_distance') ?? 10.0;
+ _backgroundTrackingEnabled = prefs.getBool('background_tracking_enabled') ?? false;
+ });
+
+ // Initialize background location service
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ final appProvider = context.read();
+ _backgroundLocationService.initialize(appProvider.connectionProvider.bleService);
+
+ // Restore background tracking state
+ if (_backgroundTrackingEnabled) {
+ _startBackgroundTracking();
+ }
+ }
+ });
+ }
+ }
+
+ Future _saveLocationSettings() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setDouble('map_gps_update_distance', _gpsUpdateDistance);
+ await prefs.setBool('background_tracking_enabled', _backgroundTrackingEnabled);
+ }
+
Future _saveThemePreference(ThemeMode theme) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme_mode', theme.name);
@@ -136,6 +171,32 @@ class _SettingsScreenState extends State {
}
}
+ Future _startBackgroundTracking() async {
+ final success = await _backgroundLocationService.startTracking(
+ distanceThreshold: _gpsUpdateDistance,
+ );
+
+ if (!success) {
+ if (mounted) {
+ setState(() {
+ _backgroundTrackingEnabled = false;
+ });
+ _saveLocationSettings();
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Failed to start background tracking. Check permissions and BLE connection.'),
+ duration: Duration(seconds: 3),
+ ),
+ );
+ }
+ }
+ }
+
+ Future _stopBackgroundTracking() async {
+ await _backgroundLocationService.stopTracking();
+ }
+
Future _clearSampleData() async {
final confirmed = await showDialog(
context: context,
@@ -195,6 +256,34 @@ class _SettingsScreenState extends State {
),
const Divider(),
+ // Location Settings Section
+ _buildSectionHeader('Location'),
+ ListTile(
+ leading: const Icon(Icons.gps_fixed),
+ title: const Text('GPS Update Distance'),
+ subtitle: Text('${_gpsUpdateDistance.toStringAsFixed(0)} meters'),
+ trailing: const Icon(Icons.chevron_right),
+ onTap: () => _showGpsDistanceDialog(),
+ ),
+ SwitchListTile(
+ secondary: const Icon(Icons.location_on),
+ title: const Text('Background Location Tracking'),
+ subtitle: const Text('Send position updates to mesh network'),
+ value: _backgroundTrackingEnabled,
+ onChanged: (value) {
+ setState(() {
+ _backgroundTrackingEnabled = value;
+ if (value) {
+ _startBackgroundTracking();
+ } else {
+ _stopBackgroundTracking();
+ }
+ });
+ _saveLocationSettings();
+ },
+ ),
+ const Divider(),
+
// About Section
_buildSectionHeader('About'),
ListTile(
@@ -305,6 +394,78 @@ class _SettingsScreenState extends State {
}
}
+ void _showGpsDistanceDialog() {
+ double tempDistance = _gpsUpdateDistance;
+ showDialog(
+ context: context,
+ builder: (context) => StatefulBuilder(
+ builder: (context, setDialogState) => AlertDialog(
+ title: const Text('GPS Update Distance'),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Position updates sent every ${tempDistance.toStringAsFixed(0)} meters',
+ style: Theme.of(context).textTheme.bodyMedium,
+ ),
+ const SizedBox(height: 16),
+ Slider(
+ value: tempDistance,
+ min: 1,
+ max: 100,
+ divisions: 99,
+ label: '${tempDistance.toStringAsFixed(0)}m',
+ onChanged: (value) {
+ setDialogState(() {
+ tempDistance = value;
+ });
+ },
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '1m',
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ Text(
+ '100m',
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Cancel'),
+ ),
+ TextButton(
+ onPressed: () {
+ setState(() {
+ _gpsUpdateDistance = tempDistance;
+ });
+ _saveLocationSettings();
+
+ // Update background tracking if active
+ if (_backgroundTrackingEnabled) {
+ _backgroundLocationService.updateDistanceThreshold(tempDistance);
+ }
+
+ Navigator.pop(context);
+ },
+ child: const Text('Save'),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
void _showThemeDialog() {
showDialog(
context: context,
diff --git a/lib/services/background_location_service.dart b/lib/services/background_location_service.dart
new file mode 100644
index 0000000..4481e20
--- /dev/null
+++ b/lib/services/background_location_service.dart
@@ -0,0 +1,182 @@
+import 'dart:async';
+import 'dart:ui';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_background_service/flutter_background_service.dart';
+import 'package:geolocator/geolocator.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'meshcore_ble_service.dart';
+
+/// Background location tracking service for SAR operations
+/// Tracks user location and sends periodic updates via MeshCore BLE
+class BackgroundLocationService {
+ static const String _prefKeyEnabled = 'background_tracking_enabled';
+ static const String _prefKeyDistance = 'background_tracking_distance';
+
+ MeshCoreBleService? _bleService;
+ bool _isInitialized = false;
+
+ /// Initialize the service with BLE service reference
+ void initialize(MeshCoreBleService bleService) {
+ _bleService = bleService;
+ _isInitialized = true;
+ }
+
+ /// Start background location tracking
+ /// Returns true if successful, false otherwise
+ Future startTracking({double distanceThreshold = 10.0}) async {
+ if (!_isInitialized || _bleService == null) {
+ return false;
+ }
+
+ // Check location permissions
+ LocationPermission permission = await Geolocator.checkPermission();
+ if (permission == LocationPermission.denied) {
+ permission = await Geolocator.requestPermission();
+ if (permission == LocationPermission.denied) {
+ return false;
+ }
+ }
+
+ if (permission == LocationPermission.deniedForever) {
+ return false;
+ }
+
+ // Save settings
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool(_prefKeyEnabled, true);
+ await prefs.setDouble(_prefKeyDistance, distanceThreshold);
+
+ // Initialize background service if not already running
+ final service = FlutterBackgroundService();
+ final isRunning = await service.isRunning();
+
+ if (!isRunning) {
+ await _initializeBackgroundService();
+ }
+
+ // Start the service
+ await service.startService();
+
+ return true;
+ }
+
+ /// Stop background location tracking
+ Future stopTracking() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setBool(_prefKeyEnabled, false);
+
+ final service = FlutterBackgroundService();
+ service.invoke('stop');
+ }
+
+ /// Update the distance threshold for location updates
+ void updateDistanceThreshold(double distance) async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setDouble(_prefKeyDistance, distance);
+
+ final service = FlutterBackgroundService();
+ service.invoke('updateDistance', {'distance': distance});
+ }
+
+ /// Initialize the background service
+ Future _initializeBackgroundService() async {
+ final service = FlutterBackgroundService();
+
+ await service.configure(
+ iosConfiguration: IosConfiguration(
+ autoStart: false,
+ onForeground: _onStart,
+ onBackground: _onIosBackground,
+ ),
+ androidConfiguration: AndroidConfiguration(
+ autoStart: false,
+ onStart: _onStart,
+ isForegroundMode: true,
+ autoStartOnBoot: false,
+ ),
+ );
+ }
+
+ /// iOS background entry point
+ @pragma('vm:entry-point')
+ static bool _onIosBackground(ServiceInstance service) {
+ WidgetsFlutterBinding.ensureInitialized();
+ DartPluginRegistrant.ensureInitialized();
+ return true;
+ }
+
+ /// Background service entry point
+ @pragma('vm:entry-point')
+ static void _onStart(ServiceInstance service) async {
+ // Ensure Flutter binding is initialized
+ DartPluginRegistrant.ensureInitialized();
+
+ Position? lastPosition;
+ StreamSubscription? positionSubscription;
+ double distanceThreshold = 10.0;
+
+ // Load settings
+ final prefs = await SharedPreferences.getInstance();
+ final enabled = prefs.getBool(_prefKeyEnabled) ?? false;
+ distanceThreshold = prefs.getDouble(_prefKeyDistance) ?? 10.0;
+
+ if (!enabled) {
+ service.stopSelf();
+ return;
+ }
+
+ // Start location tracking
+ try {
+ positionSubscription = Geolocator.getPositionStream(
+ locationSettings: LocationSettings(
+ accuracy: LocationAccuracy.best,
+ distanceFilter: distanceThreshold.toInt(),
+ ),
+ ).listen((Position position) async {
+ // Calculate distance from last position
+ if (lastPosition != null) {
+ final distance = Geolocator.distanceBetween(
+ lastPosition!.latitude,
+ lastPosition!.longitude,
+ position.latitude,
+ position.longitude,
+ );
+
+ // Only update if moved enough distance
+ if (distance < distanceThreshold) {
+ return;
+ }
+ }
+
+ // Store last position
+ lastPosition = position;
+
+ // Note: In a real implementation, we would need to communicate with
+ // the BLE service via isolate communication or shared storage.
+ // For now, this is a placeholder for the background tracking logic.
+
+ // Send location update via notification or data channel
+ service.invoke('location', {
+ 'latitude': position.latitude,
+ 'longitude': position.longitude,
+ 'timestamp': position.timestamp.millisecondsSinceEpoch,
+ });
+ });
+ } catch (e) {
+ service.stopSelf();
+ return;
+ }
+
+ // Listen for service commands
+ service.on('stop').listen((event) async {
+ await positionSubscription?.cancel();
+ service.stopSelf();
+ });
+
+ service.on('updateDistance').listen((event) {
+ if (event != null && event['distance'] != null) {
+ distanceThreshold = event['distance'] as double;
+ }
+ });
+ }
+}
diff --git a/lib/services/meshcore_ble_service.dart b/lib/services/meshcore_ble_service.dart
index 56b2f9f..3d3e9a6 100644
--- a/lib/services/meshcore_ble_service.dart
+++ b/lib/services/meshcore_ble_service.dart
@@ -386,6 +386,19 @@ class MeshCoreBleService {
await _writeData(writer.toBytes());
}
+ /// Send flood advertisement with current location
+ Future sendFloodAdvertisement({
+ required double latitude,
+ required double longitude,
+ }) async {
+ final writer = BufferWriter();
+ writer.writeByte(MeshCoreConstants.cmdSendSelfAdvert);
+ writer.writeByte(MeshCoreConstants.selfAdvertFlood);
+ writer.writeInt32LE((latitude * 10000).round());
+ writer.writeInt32LE((longitude * 10000).round());
+ await _writeData(writer.toBytes());
+ }
+
/// Dispose resources
void dispose() {
_txSubscription?.cancel();
diff --git a/lib/services/tile_cache_service.dart b/lib/services/tile_cache_service.dart
index 62165c6..24d8cf8 100644
--- a/lib/services/tile_cache_service.dart
+++ b/lib/services/tile_cache_service.dart
@@ -8,6 +8,10 @@ import '../models/map_layer.dart';
class TileCacheService {
static const String _storeName = 'meshcore_sar_tiles';
+ // Global flag to ensure ObjectBox is only initialized once
+ static bool _objectBoxInitialized = false;
+ static final _initLock = >{};
+
late final FMTCStore _store;
bool _isInitialized = false;
bool _isDownloading = false;
@@ -15,8 +19,22 @@ class TileCacheService {
Future initialize() async {
if (_isInitialized) return;
+ // Ensure we only initialize ObjectBox once globally
+ if (!_objectBoxInitialized) {
+ // Use a lock to prevent concurrent initialization attempts
+ final initFuture = _initLock.putIfAbsent('objectbox', () async {
+ try {
+ await FMTCObjectBoxBackend().initialise();
+ _objectBoxInitialized = true;
+ } catch (e) {
+ // Already initialized or error - that's okay
+ _objectBoxInitialized = true;
+ }
+ });
+ await initFuture;
+ }
+
try {
- await FMTCObjectBoxBackend().initialise();
_store = FMTCStore(_storeName);
await _store.manage.create();
_isInitialized = true;
diff --git a/macos/Podfile.lock b/macos/Podfile.lock
index e8d6057..836ff9b 100644
--- a/macos/Podfile.lock
+++ b/macos/Podfile.lock
@@ -1,42 +1,74 @@
PODS:
+ - file_picker (0.0.1):
+ - FlutterMacOS
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- FlutterMacOS (1.0.0)
- - ObjectBox (1.9.2)
+ - geolocator_apple (1.2.0):
+ - Flutter
+ - FlutterMacOS
+ - ObjectBox (4.4.1)
- objectbox_flutter_libs (0.0.1):
- FlutterMacOS
- - ObjectBox (= 1.9.2)
+ - ObjectBox (= 4.4.1)
+ - package_info_plus (0.0.1):
+ - FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
+ - share_plus (0.0.1):
+ - FlutterMacOS
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
DEPENDENCIES:
+ - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- flutter_blue_plus_darwin (from `Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
- FlutterMacOS (from `Flutter/ephemeral`)
+ - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
- objectbox_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos`)
+ - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
+ - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
+ - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
SPEC REPOS:
trunk:
- ObjectBox
EXTERNAL SOURCES:
+ file_picker:
+ :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
flutter_blue_plus_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/flutter_blue_plus_darwin/darwin
FlutterMacOS:
:path: Flutter/ephemeral
+ geolocator_apple:
+ :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin
objectbox_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/objectbox_flutter_libs/macos
+ package_info_plus:
+ :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
+ share_plus:
+ :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
+ shared_preferences_foundation:
+ :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
SPEC CHECKSUMS:
+ file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
- ObjectBox: 46757a559717ab49b1cc0612eaff82b705acf394
- objectbox_flutter_libs: ac606b51011c9ea99c0574bf2db0d27b053c750a
+ geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
+ ObjectBox: 7da4aceb5013d041bfafdbc6d744a26918b09757
+ objectbox_flutter_libs: f51d18f6a4b5965c218843373b7dc5ed5e3a2008
+ package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
+ share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
+ shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
diff --git a/pubspec.lock b/pubspec.lock
index 77e2512..3308880 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -50,7 +50,7 @@ packages:
source: hosted
version: "1.1.2"
collection:
- dependency: "direct main"
+ dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
@@ -158,6 +158,38 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_background_service:
+ dependency: "direct main"
+ description:
+ name: flutter_background_service
+ sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.0"
+ flutter_background_service_android:
+ dependency: transitive
+ description:
+ name: flutter_background_service_android
+ sha256: ca0793d4cd19f1e194a130918401a3d0b1076c81236f7273458ae96987944a87
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.1"
+ flutter_background_service_ios:
+ dependency: transitive
+ description:
+ name: flutter_background_service_ios
+ sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.3"
+ flutter_background_service_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_background_service_platform_interface
+ sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.2"
flutter_blue_plus:
dependency: "direct main"
description:
@@ -345,7 +377,7 @@ packages:
source: hosted
version: "4.1.2"
intl:
- dependency: "direct main"
+ dependency: transitive
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
diff --git a/pubspec.yaml b/pubspec.yaml
index 33a8895..3734584 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -55,10 +55,6 @@ dependencies:
geolocator: ^14.0.2
flutter_compass: ^0.8.0
- # Utilities
- intl: ^0.20.2
- collection: ^1.18.0
-
# File handling
file_picker: ^8.1.4
share_plus: ^10.1.3
@@ -70,6 +66,9 @@ dependencies:
# Package info
package_info_plus: ^8.1.2
+ # Background services
+ flutter_background_service: ^5.0.13
+
dev_dependencies:
flutter_test:
sdk: flutter