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