diff --git a/STML/stml_application/lib/main.dart b/STML/stml_application/lib/main.dart index dba6a656..1fc8f859 100644 --- a/STML/stml_application/lib/main.dart +++ b/STML/stml_application/lib/main.dart @@ -2,11 +2,11 @@ import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:fitbitter/fitbitter.dart'; import 'package:flutter/material.dart'; import 'package:memoryminder/src/features/account_creation_and_login/presentation/eula_screen.dart'; +import 'package:memoryminder/src/features/account_creation_and_login/presentation/login_screen.dart'; import 'package:memoryminder/src/features/account_creation_and_login/presentation/welcome_screen.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/service/notification_service.dart'; import 'package:memoryminder/src/utils/logger.dart'; import 'package:memoryminder/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart'; -import 'package:memoryminder/src/features/account_creation_and_login/presentation/login_screen.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:memoryminder/src/camera_manager.dart'; import 'package:memoryminder/src/data_service.dart'; @@ -19,21 +19,36 @@ import 'package:memoryminder/features/caregiver_task_management/caregiver_task_s import 'package:memoryminder/src/features/wearable-integration/health_dashboard.dart'; import 'package:memoryminder/src/features/wearable-integration/fitbit_login.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:memoryminder/firebase_options.dart'; +import 'package:memoryminder/ui/Return_Me_Home_Temp.dart'; +import 'package:memoryminder/ui/safe_zone_settings_screen.dart'; +import 'package:geolocator/geolocator.dart'; + + final storage = FlutterSecureStorage(); void main() async { - initializeLogging(); - WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(); - await dotenv.load(fileName: ".env"); - await DirectoryManager.instance.initializeDirectories(); - await DataService.instance.initializeData(); - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); - initializeData(); + try { + initializeLogging(); + WidgetsFlutterBinding.ensureInitialized(); + print("Starting Firebase initialization"); + await Firebase.initializeApp(); + print("Firebase initialized successfully"); + await Geolocator.requestPermission(); + await dotenv.load(fileName: ".env"); + await DirectoryManager.instance.initializeDirectories(); + await DataService.instance.initializeData(); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + initializeData(); + } catch (e, stackTrace) { + print("Initialization error: $e"); + print("Stack trace: $stackTrace"); + } runApp(const MyApp()); } + class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @@ -48,13 +63,15 @@ class MyApp extends StatelessWidget { initialRoute: '/loginScreen', // The initial screen when the application starts routes: { - '/welcomeScreen': (context) => WelcomeScreem(), + '/welcomeScreen': (context) => WelcomeScreen(), '/loginScreen': (context) => LoginScreen(), '/registrationScreen': (context) => RegistrationScreen(), '/eulaScreen': (context) => EulaScreen(), '/homeScreen': (context) => STMLUserDashboardScreen(), '/caregiverTaskScreen': (context) => CaregiverTaskScreen(), // Added route + '/returnMeHome': (context) => ReturnMeHomePage(), + '/safeZoneSettings': (context) => SafeZoneSettingsScreen(), '/healthMetrics': (context) => FutureBuilder( future: _loadFitbitCredentials(), builder: (context, snapshot) { diff --git a/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart b/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart index 27a3c8fa..32cf8cca 100644 --- a/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart +++ b/STML/stml_application/lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart @@ -15,14 +15,18 @@ import 'package:memoryminder/src/camera_manager.dart'; import 'package:memoryminder/src/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:memoryminder/features/caregiver_task_management/caregiver_task_screen.dart'; +import 'package:memoryminder/ui/safe_zone_settings_screen.dart'; +import 'package:memoryminder/src/safe_zone_manager.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:memoryminder/src/features/wearable-integration/fitbit_login.dart'; import 'package:memoryminder/ui/stml_calendar_screen.dart'; -import 'package:memoryminder/ui/ReturnMeHome.dart'; +import 'package:memoryminder/ui/return_me_home.dart'; // Main HomeScreen widget which is a stateless widget. class STMLUserDashboardScreen extends StatefulWidget { @override - _STMLUserDashboardScreenState createState() => _STMLUserDashboardScreenState(); + _STMLUserDashboardScreenState createState() => + _STMLUserDashboardScreenState(); } class _STMLUserDashboardScreenState extends State { @@ -31,6 +35,7 @@ class _STMLUserDashboardScreenState extends State { // To keep track of the current location LocationEntry? currentLocationEntry; + bool hasAlertBeenShown = false; @override void initState() { @@ -47,12 +52,27 @@ class _STMLUserDashboardScreenState extends State { } } - _listenToLocationChanges() { + _listenToLocationChanges() async { + LocationPermission permission = await Geolocator.checkPermission(); + + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + permission = await Geolocator.requestPermission(); + if (permission == LocationPermission.denied || + permission == LocationPermission.deniedForever) { + print("❌ Location permission denied."); + return; // Exit if still denied + } + } final locationStream = Geolocator.getPositionStream(); + locationStream.listen((Position position) async { try { List placemarks = await placemarkFromCoordinates( - position.latitude, position.longitude); + position.latitude, + position.longitude, + ); + if (placemarks.isNotEmpty) { final Placemark placemark = placemarks.first; final address = @@ -60,11 +80,10 @@ class _STMLUserDashboardScreenState extends State { if (currentLocationEntry == null || currentLocationEntry!.address != address) { - if (currentLocationEntry != null) { - if (currentLocationEntry!.endTime == null) { - currentLocationEntry!.endTime = DateTime.now(); - await LocationDatabase.instance.update(currentLocationEntry!); - } + if (currentLocationEntry != null && + currentLocationEntry!.endTime == null) { + currentLocationEntry!.endTime = DateTime.now(); + await LocationDatabase.instance.update(currentLocationEntry!); } final newEntry = @@ -74,38 +93,138 @@ class _STMLUserDashboardScreenState extends State { currentLocationEntry = newEntry; } } + + final safeZoneManager = SafeZoneManager(); + final isOutside = await safeZoneManager.isUserOutsideSafeZone(); + + if (isOutside && !hasAlertBeenShown) { + hasAlertBeenShown = true; + await _notifyCaregiverOfSafeZoneExit(position); + _sendSafeZoneAlertToFirestore(); + _showLeftSafeZoneNotification(context); + } } catch (e) { - print(e); + print('❌ Location update error: $e'); } }); } + void _sendSafeZoneAlertToFirestore() async { + try { + await FirebaseFirestore.instance.collection('notifications').add({ + 'title': "User left their safe zone", + 'timestamp': FieldValue.serverTimestamp(), + 'read': false, + 'type': 'safe_zone_exit', + // Optionally include more info: + 'userId': 'user_123', // Replace with actual user ID if available + 'message': 'The STML user exited their designated safe zone.', + }); + print('🚨 Safe zone exit alert sent to Firestore'); + } catch (e) { + print('❌ Error sending alert: $e'); + } + } + + void _showLeftSafeZoneNotification(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text("You've Left Your Safe Zone"), + content: const Text( + "Your caregiver has been notified. Do you need help getting home?"), + actions: [ + TextButton( + child: const Text("No"), + onPressed: () { + if (Navigator.of(context, rootNavigator: true).canPop()) { + Navigator.of(context, rootNavigator: true).pop(); + } + }, + ), + TextButton( + child: const Text("Yes"), + onPressed: () { + setState(() { + hasAlertBeenShown = false; + }); + Navigator.of(context).pop(); + Navigator.of(context, rootNavigator: true) + .pushNamed('/returnMeHome'); + }, + ), + ], + ); + }, + ); + } + + // Notify caregiver in Firestore when user exits safe zone + Future _notifyCaregiverOfSafeZoneExit(Position position) async { + try { + //Gets the current user's safe zone document + final safeZoneQuery = await FirebaseFirestore.instance + .collection('safe_zones') + .limit(1) + .get(); + + if (safeZoneQuery.docs.isEmpty) { + print("❌ No safe zone found for this user."); + return; + } + + final safeZoneDoc = safeZoneQuery.docs.first; + final data = safeZoneDoc.data(); + + if (!data.containsKey('careRecipientId') || data['careRecipientId'] == null) { + print("❌ careRecipientId is missing in the safe zone document."); + return; + } + final careRecipientId = data['careRecipientId']; + + // Updates that care recipient’s Firestore record + await FirebaseFirestore.instance + .collection('careRecipients') + .doc(careRecipientId) + .set({ + 'lastKnownLocation': { + 'latitude': position.latitude, + 'longitude': position.longitude, + 'timestamp': FieldValue.serverTimestamp(), + 'status': 'Exited Safe Zone', + }, + 'needsAssistance': true, + }, SetOptions(merge: true)); + + print("πŸ“ Caregiver updated for: $careRecipientId"); + } catch (e) { + print('❌ Error notifying caregiver: $e'); + } + } + @override Widget build(BuildContext context) { return Scaffold( - // Set the background color for the entire screen - extendBodyBehindAppBar: true, - extendBody: true, - // Setting up the app bar at the top of the screen - appBar: const CustomAppBar( - title: 'My Dashboard', - ), - // Main content of the screen - body: Container( - - child: Column( - children: [ - const Padding( - padding: EdgeInsets.fromLTRB(16.0, 140, 16.0, 25), - child: Text( - 'Helping you remember the important things.\n Choose a feature to get started!', - style: TextStyle( - fontSize: 16.0, - color: Colors.black54, - ), - textAlign: TextAlign.center, + extendBodyBehindAppBar: true, + extendBody: true, + appBar: const CustomAppBar( + title: 'My Dashboard', + ), + body: Container( + child: Column( + children: [ + const Padding( + padding: EdgeInsets.fromLTRB(16.0, 140, 16.0, 25), + child: Text( + 'Helping you remember the important things.\n Choose a feature to get started!', + style: TextStyle( + fontSize: 16.0, + color: Colors.black54, ), + textAlign: TextAlign.center, ), + // Grid view to display multiple options/buttons Expanded( @@ -117,6 +236,19 @@ class _STMLUserDashboardScreenState extends State { childAspectRatio: 1.30, padding: const EdgeInsets.all(26.0), children: [ + // adding return me home button + _buildElevatedButton( + context: context, + icon: Icon(Icons.home_filled, + size: iconSize, color: Colors.black54), + text: 'Take Me Home', + screen: ProfileScreen(), + keyName: "TakeMeHomeButtonKey", + backgroundColor: + const Color(0xFF000000).withOpacity(0.30), + // below may cause conflicts - commented out for now + //onPressedOverride: () { + //showModalBottomSheet( // Using the helper function to build each button in the grid _buildElevatedButton( context: context, @@ -183,6 +315,40 @@ class _STMLUserDashboardScreenState extends State { const Color(0xFFFFFFFF).withOpacity(0.30)), _buildElevatedButton( context: context, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (BuildContext context) { + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.directions_walk), + title: const Text('Return Me Home'), + onTap: () { + Navigator.pop(context); + Navigator.of(context, rootNavigator: true) + .pushNamed('/returnMeHome'); + }, + ), + ListTile( + leading: const Icon(Icons.shield), + title: const Text('Set Safe Zone'), + onTap: () { + Navigator.pop(context); + Navigator.of(context, rootNavigator: true) + .pushNamed('/safeZoneSettings'); + }, + ), + ], + ), + ); + }, + ); + }, + ), icon: Icon(Icons.task_alt, size: iconSize, color: Colors.black54), text: 'Caregiver Tasks', @@ -202,15 +368,14 @@ class _STMLUserDashboardScreenState extends State { ], ), ), - ], - ), + ), + ], ), - - // Bottom navigation bar with multiple options for quick navigation - bottomNavigationBar: UiUtils.createBottomNavigationBar(context)); + ), + bottomNavigationBar: UiUtils.createBottomNavigationBar(context), + ); } - // Helper function to create each button for the GridView Widget _buildElevatedButton({ required BuildContext context, @@ -218,6 +383,7 @@ class _STMLUserDashboardScreenState extends State { required String text, Widget? screen, String? routeName, + VoidCallback? onPressedOverride, required String keyName, required Color backgroundColor, }) { @@ -233,14 +399,14 @@ class _STMLUserDashboardScreenState extends State { ), ), onPressed: () { - if (routeName != null) { - Navigator.pushNamed(context, routeName); // Use named route if provided - } - else if (screen != null) - { + if (onPressedOverride != null) { + onPressedOverride!(); // βœ… Use the override logic + } else if (routeName != null) { + Navigator.pushNamed(context, routeName); + } else if (screen != null) { Navigator.push( context, - MaterialPageRoute(builder: (context) => screen), // Default behavior + MaterialPageRoute(builder: (context) => screen), ); } }, diff --git a/STML/stml_application/lib/src/safe_zone_manager.dart b/STML/stml_application/lib/src/safe_zone_manager.dart new file mode 100644 index 00000000..520b6fdd --- /dev/null +++ b/STML/stml_application/lib/src/safe_zone_manager.dart @@ -0,0 +1,53 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:geolocator/geolocator.dart'; +import 'dart:math'; + +class SafeZoneManager { + static const double defaultRadiusMeters = 200; + + Future isUserOutsideSafeZone() async { + final prefs = await SharedPreferences.getInstance(); + final lat = prefs.getDouble('safe_zone_lat'); + final lng = prefs.getDouble('safe_zone_lng'); + + if (lat == null || lng == null) return false; // Safe zone not set + + final currentPosition = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.high, + ); + + final distance = _calculateDistanceInMeters( + currentPosition.latitude, + currentPosition.longitude, + lat, + lng, + ); + + return distance > defaultRadiusMeters; + } + + double _calculateDistanceInMeters( + double lat1, + double lon1, + double lat2, + double lon2, + ) { + const earthRadius = 6371000; // in meters + final dLat = _degreesToRadians(lat2 - lat1); + final dLon = _degreesToRadians(lon2 - lon1); + + final a = + sin(dLat / 2) * sin(dLat / 2) + + cos(_degreesToRadians(lat1)) * + cos(_degreesToRadians(lat2)) * + sin(dLon / 2) * + sin(dLon / 2); + + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + return earthRadius * c; + } + + double _degreesToRadians(double degrees) { + return degrees * pi / 180; + } +} diff --git a/STML/stml_application/lib/ui/ReturnMeHome.dart b/STML/stml_application/lib/ui/return_me_home.dart similarity index 100% rename from STML/stml_application/lib/ui/ReturnMeHome.dart rename to STML/stml_application/lib/ui/return_me_home.dart diff --git a/STML/stml_application/lib/ui/safe_zone_settings_screen.dart b/STML/stml_application/lib/ui/safe_zone_settings_screen.dart new file mode 100644 index 00000000..88c363c3 --- /dev/null +++ b/STML/stml_application/lib/ui/safe_zone_settings_screen.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + + +class SafeZoneSettingsScreen extends StatefulWidget { + const SafeZoneSettingsScreen({Key? key}) : super(key: key); + + @override + State createState() => _SafeZoneSettingsScreenState(); +} + +class _SafeZoneSettingsScreenState extends State { + final TextEditingController _addressController = TextEditingController(); + String? _savedAddress; + LatLng? _savedCoordinates; + bool _isLoading = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _loadSafeZone(); + } + + Future _loadSafeZone() async { + final prefs = await SharedPreferences.getInstance(); + final address = prefs.getString('safe_zone_address'); + final lat = prefs.getDouble('safe_zone_lat'); + final lng = prefs.getDouble('safe_zone_lng'); + + if (address != null && lat != null && lng != null) { + setState(() { + _addressController.text = address; + _savedAddress = address; + _savedCoordinates = LatLng(lat, lng); + }); + } + } + + Future _convertAndSaveAddress() async { + final address = _addressController.text.trim(); + if (address.isEmpty) return; + + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + try { + final locations = await locationFromAddress(address); + final location = locations.first; + + final lat = location.latitude; + final lng = location.longitude; + + final userId = FirebaseAuth.instance.currentUser?.uid; + if (userId == null) throw 'User not authenticated'; + + await FirebaseFirestore.instance + .collection('safe_zones') + .doc(userId) + .set({ + 'address': address, + 'latitude': lat, + 'longitude': lng, + 'radius_meters': 200, + 'timestamp': FieldValue.serverTimestamp(), + 'careRecipientId': userId, + }); + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('safe_zone_address', address); + await prefs.setDouble('safe_zone_lat', lat); + await prefs.setDouble('safe_zone_lng', lng); + + setState(() { + _savedAddress = address; + _savedCoordinates = LatLng(lat, lng); + _isLoading = false; + }); + } catch (e) { + setState(() { + _errorMessage = 'Error: ${e.toString()}'; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Set Your Safe Zone")), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextField( + controller: _addressController, + decoration: InputDecoration( + labelText: 'Enter your address', + suffixIcon: IconButton( + icon: const Icon(Icons.check), + onPressed: _convertAndSaveAddress, + ), + ), + ), + const SizedBox(height: 20), + if (_isLoading) const Center(child: CircularProgressIndicator()), + if (_errorMessage != null) + Text( + _errorMessage!, + style: const TextStyle(color: Colors.red), + ), + if (_savedAddress != null && _savedCoordinates != null) + Container( + margin: const EdgeInsets.only(top: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "βœ… Safe Zone Set!", + style: TextStyle( + color: Colors.green, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + Text("Address: $_savedAddress"), + Text( + "Lat: ${_savedCoordinates!.latitude}, Lng: ${_savedCoordinates!.longitude}", + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class LatLng { + final double latitude; + final double longitude; + LatLng(this.latitude, this.longitude); +}