diff --git a/.gitignore b/.gitignore index 3a83c2f0..1b90f0ec 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..12ef85a0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "C:/Users/ludov/Desktop/UMGC stml/spring2025/STML/stml_application/linux" +} \ No newline at end of file diff --git a/STML/stml_application/ios/Runner/Info.plist b/STML/stml_application/ios/Runner/Info.plist index 1167b176..6275ef4e 100644 --- a/STML/stml_application/ios/Runner/Info.plist +++ b/STML/stml_application/ios/Runner/Info.plist @@ -58,6 +58,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSMicrophoneUsageDescription + We need microphone access to record audio for memory notes. + NSDocumentsFolderUsageDescription + This app needs access to save and read audio transcripts. CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/STML/stml_application/lib/services/notification_service.dart b/STML/stml_application/lib/services/notification_service.dart new file mode 100644 index 00000000..ed055b7a --- /dev/null +++ b/STML/stml_application/lib/services/notification_service.dart @@ -0,0 +1,250 @@ +//notification_service.dart +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:geocoding/geocoding.dart'; +import 'package:memoryminder/ui/location_history_screen.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; + +class NotificationService { + final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance; + final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + final FirebaseAnalytics _analytics = FirebaseAnalytics.instance; + + String? _caregiverToken; + String? _lastRequestId; + + String? get lastRequestId => _lastRequestId; + + Future initialize() async { + // Get the FCM token for the current device + _caregiverToken = await _firebaseMessaging.getToken(); + print("Caregiver Token: $_caregiverToken"); + + // Log token generation for analytics + await _analytics.logEvent( + name: 'fcm_token_generated', + parameters: { + 'user_type': 'caregiver', + }, + ); + + // Listen for new tokens (in case the token changes) + _firebaseMessaging.onTokenRefresh.listen((newToken) { + _caregiverToken = newToken; + print("New Caregiver Token: $_caregiverToken"); + + // Log token refresh for analytics + _analytics.logEvent( + name: 'fcm_token_refreshed', + parameters: { + 'user_type': 'caregiver', + }, + ); + }); + + // Configure local notifications + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + final InitializationSettings initializationSettings = + InitializationSettings( + android: initializationSettingsAndroid, + ); + await _flutterLocalNotificationsPlugin.initialize(initializationSettings); + + // Listen for incoming messages + FirebaseMessaging.onMessage.listen((RemoteMessage message) { + print("Message received: ${message.notification?.title}"); + _showNotification(message); + + // Log message received for analytics + _analytics.logEvent( + name: 'notification_received', + parameters: { + 'notification_type': message.data['type'] ?? 'unknown', + 'has_notification': message.notification != null, + }, + ); + }); + } + + Future _showNotification(RemoteMessage message) async { + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'your_channel_id', // Notification channel ID + 'your_channel_name', // Notification channel name + importance: Importance.max, + priority: Priority.high, + ); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + + await _flutterLocalNotificationsPlugin.show( + 0, // Notification ID + message.notification?.title, // Notification title + message.notification?.body, // Notification body + platformChannelSpecifics, + ); + + // Log notification shown for analytics + await _analytics.logEvent( + name: 'notification_displayed', + parameters: { + 'notification_title': message.notification?.title ?? 'No title', + }, + ); + } + + Future sendHelpNotification(LocationEntry? currentLocationEntry) async { + try { + // Start timer for performance tracking + final startTime = DateTime.now(); + + // Prepare location data + Map? locationData; + + if (currentLocationEntry != null) { + try { + // Try to get the current coordinates + Position currentPosition = await Geolocator.getCurrentPosition(); + + // Create the data map with the stored address and current coordinates + locationData = { + 'address': currentLocationEntry.address, + 'latitude': currentPosition.latitude, + 'longitude': currentPosition.longitude, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + + // Log successful location capture + await _analytics.logEvent( + name: 'emergency_location_captured', + parameters: { + 'method': 'geolocator', + 'has_coordinates': true, + }, + ); + } catch (e) { + // Log location error + await _analytics.logEvent( + name: 'emergency_location_error', + parameters: { + 'error_type': 'geolocator_error', + 'error_message': e.toString(), + }, + ); + + // Fallback: try to geocode the address to get coordinates + try { + List locations = + await locationFromAddress(currentLocationEntry.address); + if (locations.isNotEmpty) { + locationData = { + 'address': currentLocationEntry.address, + 'latitude': locations.first.latitude, + 'longitude': locations.first.longitude, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + + // Log successful geocoding + await _analytics.logEvent( + name: 'emergency_location_captured', + parameters: { + 'method': 'geocoding', + 'has_coordinates': true, + }, + ); + } else { + // If geocoding fails, only send the address + locationData = { + 'address': currentLocationEntry.address, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + + // Log geocoding with no results + await _analytics.logEvent( + name: 'emergency_location_captured', + parameters: { + 'method': 'geocoding', + 'has_coordinates': false, + }, + ); + } + } catch (e) { + // Log geocoding error + await _analytics.logEvent( + name: 'emergency_location_error', + parameters: { + 'error_type': 'geocoding_error', + 'error_message': e.toString(), + }, + ); + + // As a last resort, only send the address + locationData = { + 'address': currentLocationEntry.address, + 'timestamp': currentLocationEntry.startTime.toIso8601String() + }; + } + } + } else { + // Log no location available + await _analytics.logEvent( + name: 'emergency_location_missing', + ); + } + + // Call the Cloud Function + final HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable('sendHelpAlert'); + final result = await callable.call({ + 'caregiverToken': _caregiverToken, + 'location': locationData, + 'userId': FirebaseAuth.instance.currentUser?.uid, + 'userName': FirebaseAuth.instance.currentUser?.displayName + }); + + // Save the request ID for later reference + _lastRequestId = result.data['requestId']; + + // Calculate time taken + final endTime = DateTime.now(); + final duration = endTime.difference(startTime).inMilliseconds; + + // Create parameters map without nullable values + final Map analyticsParams = { + 'success': result.data['success'] ?? false, + 'has_location': currentLocationEntry != null, + 'processing_time_ms': duration, + }; + + // Add requestId only if it's not null + if (_lastRequestId != null) { + analyticsParams['request_id'] = _lastRequestId!; + } + + // Log complete event with success/failure + await _analytics.logEvent( + name: 'emergency_alert_sent', + parameters: analyticsParams, + ); + + return result.data['success'] ?? false; + } catch (e) { + print("Error sending notification: $e"); + + // Log error event + await _analytics.logEvent( + name: 'emergency_alert_error', + parameters: { + 'error_message': e.toString(), + }, + ); + + return false; + } + } +} diff --git a/STML/stml_application/lib/src/features/account_creation_and_login/presentation/login_screen.dart b/STML/stml_application/lib/src/features/account_creation_and_login/presentation/login_screen.dart index 5d5ba93f..f704c7d5 100644 --- a/STML/stml_application/lib/src/features/account_creation_and_login/presentation/login_screen.dart +++ b/STML/stml_application/lib/src/features/account_creation_and_login/presentation/login_screen.dart @@ -6,6 +6,7 @@ Author: Eyerusalme (Jerry) import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:memoryminder/src/features/account_creation_and_login/presentation/eula_screen.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart'; import 'package:memoryminder/src/utils/permission_manager.dart'; import 'registration_screen.dart'; @@ -34,7 +35,7 @@ class _LoginScreenState extends State { if (didAuthenticate) { Navigator.pushReplacement( context, - MaterialPageRoute(builder: (context) => HomeScreen()), + MaterialPageRoute(builder: (context) => STMLUserDashboardScreen()), ); } else { ScaffoldMessenger.of(context).showSnackBar( @@ -50,7 +51,6 @@ class _LoginScreenState extends State { } Future _loginWithEmail() async { - setState(() { _isAuthenticating = true; }); @@ -80,7 +80,6 @@ class _LoginScreenState extends State { PermissionManager.checkIfLocationServiceIsActive(context); return Scaffold( body: Container( - child: Center( child: Padding( padding: @@ -111,15 +110,14 @@ class _LoginScreenState extends State { Image.asset('assets/welcome_image.png', height: 300), // Add a nice image TextFormField( - controller: _emailController, - decoration: InputDecoration(labelText: 'Email'), - keyboardType: TextInputType.emailAddress, - validator: (value) { - if(value ==null || value.isEmpty) { - return 'Please enter email'; - } - } - ), + controller: _emailController, + decoration: InputDecoration(labelText: 'Email'), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter email'; + } + }), SizedBox(height: 10), TextFormField( controller: _passwordController, @@ -145,10 +143,16 @@ class _LoginScreenState extends State { ? CircularProgressIndicator() : ElevatedButton( onPressed: _loginWithEmail, - child: Text("Login", style: TextStyle(fontSize: 18, color: Colors.white),), + child: Text( + "Login", + style: + TextStyle(fontSize: 18, color: Colors.white), + ), style: ElevatedButton.styleFrom( - backgroundColor: const Color.fromARGB(255, 2, 63, 129), - padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15), + backgroundColor: + const Color.fromARGB(255, 2, 63, 129), + padding: EdgeInsets.symmetric( + horizontal: 50, vertical: 15), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40), ), @@ -160,11 +164,18 @@ class _LoginScreenState extends State { ElevatedButton.icon( onPressed: _authenticateWithBiometrics, - icon: Icon(Icons.fingerprint, color: Colors.white,), - label: Text("Login with Biometrics", style: TextStyle(fontSize: 18, color: Colors.white), ), + icon: Icon( + Icons.fingerprint, + color: Colors.white, + ), + label: Text( + "Login with Biometrics", + style: TextStyle(fontSize: 18, color: Colors.white), + ), style: ElevatedButton.styleFrom( backgroundColor: const Color.fromARGB(255, 2, 63, 129), - padding: EdgeInsets.symmetric(horizontal: 50, vertical: 15), + padding: + EdgeInsets.symmetric(horizontal: 50, vertical: 15), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(40), ), @@ -176,7 +187,7 @@ class _LoginScreenState extends State { Navigator.push( context, MaterialPageRoute( - builder: (context) => RegistrationScreen()), + builder: (context) => EulaScreen()), ); }, child: Row( diff --git a/STML/stml_application/lib/src/features/account_creation_and_login/presentation/registration_screen.dart b/STML/stml_application/lib/src/features/account_creation_and_login/presentation/registration_screen.dart index d2fa5003..6e3b4960 100644 --- a/STML/stml_application/lib/src/features/account_creation_and_login/presentation/registration_screen.dart +++ b/STML/stml_application/lib/src/features/account_creation_and_login/presentation/registration_screen.dart @@ -85,7 +85,6 @@ class _RegistrationScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: const Color(0XFF880E4F), extendBodyBehindAppBar: true, extendBody: true, appBar: AppBar( @@ -97,12 +96,7 @@ class _RegistrationScreenState extends State { const Text('Registration', style: TextStyle(color: Colors.black54)), ), body: Container( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage("assets/images/background.jpg"), - fit: BoxFit.cover, - ), - ), + child: Center( child: Padding( padding: diff --git a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/app_bar.dart b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/app_bar.dart index e997970b..f96a2647 100644 --- a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/app_bar.dart +++ b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/app_bar.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:memoryminder/src/features/account_creation_and_login/presentation/login_screen.dart'; import 'package:memoryminder/ui/profile_screen.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @@ -67,29 +68,20 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { // Widgets on the right side of the AppBar actions: [ // First page icon to navigate back - IconButton( + + +// First page icon to navigate back + IconButton( icon: const Icon( - Icons.settings, - color: Colors.white70, + Icons.logout, + color: Colors.white, ), onPressed: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => ProfileScreen()), - ); - }, + MaterialPageRoute(builder: (context) => LoginScreen()), + ); }, ), - -// First page icon to navigate back - /* IconButton( - icon: const Icon( - Icons.first_page, - color: Colors.black87, - ), - onPressed: () { - Navigator.pop(context); - }, - ),*/ ], ); } diff --git a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/care_recipient_profile.dart b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/care_recipient_profile.dart index c9a4e730..c4038562 100644 --- a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/care_recipient_profile.dart +++ b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/care_recipient_profile.dart @@ -1,6 +1,7 @@ // ignore_for_file: avoid_print, prefer_const_constructors // Imported libraries and packages +import 'package:memoryminder/features/caregiver_task_management/caregiver_task_screen.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/presentation/add_care_recipient.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/presentation/app_bar.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart'; @@ -122,6 +123,22 @@ class CareRecipientProfileScreenState extends State screen: DementiaResourcesScreen(loc: careRecipientLocation), keyName: "DementiaResourcesButtonKey", ), + _buildElevatedButton( + context: context, + icon: Icon(Icons.task_alt, + size: iconSize, color: Color.fromARGB(255, 2, 63, 129)), + text: 'Tasks', + screen: CaregiverTaskScreen(), + keyName: "CaregiverTaskButtonKey", + ), + _buildElevatedButton( + context: context, + icon: Icon(Icons.language_outlined, + size: iconSize, color: Color.fromARGB(255, 2, 63, 129)), + text: 'Language Preferences', + keyName: "CaregiverTaskButtonKey", + + ), ], ), ), @@ -133,6 +150,10 @@ class CareRecipientProfileScreenState extends State bottomNavigationBar: UiUtils.createBottomNavigationBar(context)); } + void _showLanguageDialog(BuildContext context) { + +} + // Helper function to create each button for the GridView Widget _buildElevatedButton({ required BuildContext context, @@ -141,6 +162,7 @@ class CareRecipientProfileScreenState extends State Widget? screen, String? routeName, required String keyName, + VoidCallback? functionName, }) { return ElevatedButton( key: Key(keyName), diff --git a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart index 26949d01..21426b0b 100644 --- a/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart +++ b/STML/stml_application/lib/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart @@ -10,10 +10,10 @@ import 'package:memoryminder/src/features/caregiver-dashboard/presentation/care_ import 'package:memoryminder/src/features/caregiver-dashboard/service/manage_care_recipient_service.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/service/notification_service.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/service/notification_stream_service.dart'; -import 'package:memoryminder/ui/profile_screen.dart'; import 'package:memoryminder/src/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; // Main HomeScreen widget which is a stateless widget. class CaregiverDashboardScreen extends StatefulWidget { @@ -33,6 +33,41 @@ class _CaregiverDashboardScreen extends State { _careRecipientData = ManageCareRecipientService().getAllCareRecipients(); } + Future _callEmergencyNumber() async { + const String emergencyNumber = '911'; + final Uri phoneUri = Uri( + scheme: 'tel', + path: emergencyNumber, + ); + + try { + if (await canLaunchUrl(phoneUri)) { + await launchUrl( + phoneUri, + mode: LaunchMode.externalApplication, + ); + } else { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Cannot launch dialer for $emergencyNumber'), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Emergency call failed: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + @override void dispose() { NotificationStreamService().dispose(); @@ -43,19 +78,10 @@ class _CaregiverDashboardScreen extends State { Widget build(BuildContext context) { return Scaffold( extendBody: true, - // Setting up the app bar at the top of the screen appBar: const CustomAppBar( title: 'Caregiver Dashboard', ), - body: Container( - /*decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/images/background.jpg'), - // Replace with your image path - fit: BoxFit.cover, - ), - ),*/ child: Column( children: [ const Padding( @@ -105,14 +131,13 @@ class _CaregiverDashboardScreen extends State { child: Container( decoration: BoxDecoration( color: Colors.transparent, - borderRadius: BorderRadius.circular(10), // Rounded corners + borderRadius: BorderRadius.circular(10), ), child: _buildNotificationList( context: context, ), ), ), - const Divider( color: Colors.black54, thickness: 2, @@ -120,7 +145,32 @@ class _CaregiverDashboardScreen extends State { indent: 20, endIndent: 20, ), - + const Divider( + color: Colors.black54, + thickness: 2, + height: 10, + indent: 20, + endIndent: 20, + ), + ElevatedButton( + onPressed: _callEmergencyNumber, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 16), + ), + child: const Text( + 'Emergency Call', + style: TextStyle(fontSize: 18, color: Colors.white), + ), + ), + const Divider( + color: Colors.black54, + thickness: 2, + height: 10, + indent: 20, + endIndent: 20, + ), Padding( padding: EdgeInsets.fromLTRB(2.0, 2, 2.0, 2), child: Column( @@ -138,7 +188,8 @@ class _CaregiverDashboardScreen extends State { onPressed: () { Navigator.push( context, - MaterialPageRoute(builder: (context) => AddCareRecipientForm()), + MaterialPageRoute( + builder: (context) => AddCareRecipientForm()), ); }, icon: Icon( @@ -149,18 +200,16 @@ class _CaregiverDashboardScreen extends State { style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.black, - padding: EdgeInsets.fromLTRB(2.0, 2, 16.0, 2), // Apply padding here + padding: EdgeInsets.fromLTRB(2.0, 2, 16.0, 2), ), ), ], ), ), - - Expanded( - child: _buildCareRecipientsGrid( - context: context, - ), + child: _buildCareRecipientsGrid( + context: context, + ), ), const Divider( color: Colors.black, @@ -172,8 +221,6 @@ class _CaregiverDashboardScreen extends State { ], ), ), - - // Bottom navigation bar with multiple options for quick navigation bottomNavigationBar: UiUtils.createBottomNavigationBar(context)); } @@ -187,61 +234,61 @@ class _CaregiverDashboardScreen extends State { final data = snapshot.data!; return GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, // Adjust as needed - childAspectRatio: 1.30, - mainAxisSpacing: 10, - crossAxisSpacing: 10 - ), + crossAxisCount: 3, + childAspectRatio: 1.0, + mainAxisSpacing: 3, + crossAxisSpacing: 3), itemCount: data.length, itemBuilder: (context, index) { final item = data[index]; - final String labelText = '${item['firstName'].toString()} ${item['lastName'].toString()}'; + final String labelText = + '${item['firstName'].toString()} ${item['lastName'].toString()}'; final careRecipient = CareRecipient.fromMap(item); - return InkWell( // Or InkWell for ripple effect - onTap: () { - // Handle item click - print('Item ${item['firstName'].toString()} clicked'); - Navigator.push( - context, - MaterialPageRoute(builder: (context) => CareRecipientProfileScreen(careRecipientId: item['itemId'].toString(), careRecipientData: careRecipient.toMap())), - ); - }, - borderRadius: BorderRadius.circular(12.0), - child: Container( - decoration: BoxDecoration( - color: Colors.lightBlue[100], - borderRadius: BorderRadius.circular(12.0), - boxShadow: [ - BoxShadow( - color: Colors.lightBlueAccent, - spreadRadius: 1, - blurRadius: 3, - offset: const Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.person, - size: 40.0, - color: Color.fromARGB(255, 2, 63, 129), - ), - const SizedBox(height: 8.0), - Text( - labelText, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.w500, - color: Colors.black87, + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CareRecipientProfileScreen( + careRecipientId: item['itemId'].toString(), + careRecipientData: careRecipient.toMap())), + ); + }, + borderRadius: BorderRadius.circular(12.0), + child: Container( + decoration: BoxDecoration( + color: Colors.lightGreen[100], + borderRadius: BorderRadius.circular(12.0), + boxShadow: [ + BoxShadow( + color: Colors.lightGreen.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 3, + offset: const Offset(0, 2), + ), + ], + ), + padding: const EdgeInsets.all(3.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.person, + size: 40.0, + color: Colors.green, + ), + const SizedBox(height: 8.0), + Text( + labelText, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w500, + color: Colors.black87, ), ), ], ), - ) - ); + )); }, ); } else if (snapshot.hasError) { @@ -270,7 +317,6 @@ class _CaregiverDashboardScreen extends State { return Center(child: Text('No notifications available')); } - // List of notifications List> notifications = snapshot.data!; return Container( @@ -296,11 +342,9 @@ class _CaregiverDashboardScreen extends State { color: Colors.green) : Icon(Icons.notifications, color: Colors.red), onTap: () { - // Handle notification tap and mark as read notificationService .markNotificationAsRead(notification['id']); }, - ); }, )); @@ -308,5 +352,3 @@ class _CaregiverDashboardScreen extends State { ); } } - - diff --git a/STML/stml_application/lib/src/features/sensitive_information_detection/presentation/audio_screen.dart b/STML/stml_application/lib/src/features/sensitive_information_detection/presentation/audio_screen.dart index 8568dfdb..fd58e37f 100644 --- a/STML/stml_application/lib/src/features/sensitive_information_detection/presentation/audio_screen.dart +++ b/STML/stml_application/lib/src/features/sensitive_information_detection/presentation/audio_screen.dart @@ -1,180 +1,137 @@ // ignore_for_file: avoid_print, prefer_const_constructors -// Importing required packages and screens. import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:memoryminder/src/data_service.dart'; import 'package:memoryminder/src/features/sensitive_information_detection/domain/audio.dart'; import 'package:memoryminder/src/s3_connection.dart'; import 'package:flutter/material.dart'; import 'dart:convert'; -import 'package:memoryminder/src/typingIndicator.dart'; import 'package:memoryminder/src/utils/ui_utils.dart'; import 'package:intl/intl.dart'; -import 'package:flutter_sound/flutter_sound.dart'; +import 'package:record/record.dart'; -// Permission handler is used for handling permissions like microphone and storage access. import 'package:permission_handler/permission_handler.dart'; import 'dart:async'; import 'dart:io'; -/// Path provider helps in getting system directory paths to store the recorded audio. import 'package:path_provider/path_provider.dart'; - -// Importing AWS Transcribe API and s3 bucket import 'package:aws_transcribe_api/transcribe-2017-10-26.dart' as trans; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; - -// Record button glow effect import 'package:avatar_glow/avatar_glow.dart'; const API_URL = 'https://api.openai.com/v1/chat/completions'; -final API_KEY = dotenv.env['OPEN_AI_API_KEY']; // Replace with your API key +final API_KEY = dotenv.env['OPEN_AI_API_KEY']; + +final String _bucketName = dotenv.env['videoS3Bucket']!; +final service = trans.TranscribeService( + region: dotenv.env['region']!, + credentials: trans.AwsClientCredentials( + accessKey: dotenv.env['accessKey']!, + secretKey: dotenv.env['secretKey']!, + )); -/// AudioScreen widget provides the main interface for audio recording. class AudioScreen extends StatefulWidget { @override _AudioScreenState createState() => _AudioScreenState(); } class _AudioScreenState extends State { - // FlutterSoundRecorder is responsible for recording audio. - FlutterSoundRecorder? _recorder; - - // FlutterSoundPlayer is responsible for playing back the recorded audio. - FlutterSoundPlayer? _player; - - // Flags to track if recording or playback is currently in progress. + final AudioRecorder _recorder = AudioRecorder(); bool _isRecording = false; bool _isPlaying = false; - bool _isPaused = false; - - // Flag to track if transcription is loading bool _isTranscribing = false; - - // Variable to track the duration of the current recording. - Duration _duration = const Duration(seconds: 0); - - // This variable will store the path where the recorded audio will be saved. + Duration _duration = Duration.zero; String? _pathToSaveRecording; - - // Timer is used to update the duration of the recording in real-time. + String? _recordingKey; Timer? _timer; - - // Variables from env for s3 - final _bucketName = dotenv.env['videoS3Bucket']; - final service = trans.TranscribeService( - region: dotenv.env['region']!, - credentials: trans.AwsClientCredentials( - accessKey: dotenv.env['accessKey']!, - secretKey: dotenv.env['secretKey']!, - ), - ); - var key2 = ''; - - S3Service s3Connection = S3Service(); - String transcription = ''; String transcriptionSummary = ''; - Audio? audio; int? audioId; + String _currentStatus = "Idle"; @override void initState() { super.initState(); - - // Initializing recorder and player instances. - _recorder = FlutterSoundRecorder(); - _player = FlutterSoundPlayer(); - - /// Setting up the recorder by checking permissions. _initializeRecorder(); - _startRecording(); - } - - FutureOr _showPermissionDialogue() { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Permission Required"), - content: const Text( - "MemoryMinder audio recording features require access to your device's microphone. Please allow Microphone access in your device settings."), - actions: [ - TextButton( - child: const Text('Cancel'), - onPressed: () { - Navigator.pop(context); - }, - ), - TextButton( - child: const Text('Settings'), - onPressed: () { - Navigator.pop(context); - openAppSettings(); - }, - ), - ], - )); } - /// This function initializes the recorder by checking necessary permissions. Future _initializeRecorder() async { bool permissionsGranted = await _requestPermissions(); - if (!permissionsGranted) { _showPermissionDialogue(); - return; } - await _recorder!.openRecorder(); } - /// This function requests necessary permissions for audio recording and storage. + Future _showPermissionDialogue() async { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Permission Required"), + content: const Text( + "MemoryMinder audio recording features require access to your device's microphone. Please allow Microphone access in your device settings."), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.pop(context), + ), + TextButton( + child: const Text('Settings'), + onPressed: () { + Navigator.pop(context); + openAppSettings(); + }, + ), + ], + ), + ); + } + Future _requestPermissions() async { final micStatus = await Permission.microphone.request(); final storageStatus = Platform.isAndroid ? await Permission.manageExternalStorage.request() : PermissionStatus.granted; - return micStatus.isGranted && storageStatus.isGranted; } @override void dispose() { - // Cleanup operations: It's important to release resources to prevent memory leaks. - _recorder!.closeRecorder(); - _player?.closePlayer(); _timer?.cancel(); super.dispose(); } - // Function to handle the starting of audio recording. Future _startRecording() async { bool permissionsGranted = await _requestPermissions(); if (!permissionsGranted) { _showPermissionDialogue(); return; } + Directory appDocDirectory = await getApplicationDocumentsDirectory(); - key2 = DateTime.now().millisecondsSinceEpoch.toString(); + _recordingKey = DateTime.now().millisecondsSinceEpoch.toString(); _pathToSaveRecording = - '${appDocDirectory.path}/files/audios/$key2.wav'; // creates unique name - debugPrint('initial app directory $appDocDirectory'); + '${appDocDirectory.path}/files/audios/$_recordingKey.wav'; - await _recorder!.startRecorder( - toFile: _pathToSaveRecording, - codec: Codec.pcm16WAV, + await _recorder.start( + const RecordConfig( + encoder: AudioEncoder.wav, sampleRate: 16000, - numChannels: 1, - bitRate: 140000); + bitRate: 128000, + ), + path: _pathToSaveRecording!, + ); + setState(() { _isRecording = true; + _currentStatus = "Recording..."; + _duration = Duration.zero; }); - /// Timer to periodically update the duration of the audio recording in the UI. - _timer = Timer.periodic(Duration(seconds: 1), (Timer t) { + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { if (_isRecording) { setState(() { - _duration = _duration + Duration(seconds: 1); + _duration += const Duration(seconds: 1); }); } else { _timer?.cancel(); @@ -183,140 +140,84 @@ class _AudioScreenState extends State { } Future _stopRecording() async { - await _recorder!.stopRecorder(); + await _recorder.stop(); + setState(() { _isRecording = false; + _currentStatus = "Stopped"; }); - _timer?.cancel(); - // Call Transcription after stopping the recording - final s3UploadUrl = - await s3Connection.addAudioToS3(key2, _pathToSaveRecording!); - - if (s3UploadUrl != null && s3UploadUrl.isNotEmpty) { - _transcribeAudio(s3UploadUrl); - } else { - print("Error: s3UploadUrl is null or empty."); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Upload failed: No S3 URL returned')), - ); - } - } - // Function to handle starting the playback of the recorded audio. - Future _startPlayback() async { - if (_pathToSaveRecording == null || _pathToSaveRecording!.isEmpty) { - print("🚨 Error: No file path found for playback."); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Playback failed: No recording found')), - ); - return; - } - - File recordedFile = File(_pathToSaveRecording!); + _timer?.cancel(); - if (!recordedFile.existsSync() || recordedFile.lengthSync() == 0) { - print("🚨 Error: The file does not exist or is empty."); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Playback failed: File is missing or corrupted')), - ); - return; - } + if (_pathToSaveRecording != null && _recordingKey != null) { + final s3UploadUrl = + await S3Service().addAudioToS3(_recordingKey!, _pathToSaveRecording!); - try { - // Ensure the player is open - if (!_player!.isOpen()) { - await _player!.openPlayer(); + if (s3UploadUrl != null && s3UploadUrl.isNotEmpty) { + _transcribeAudio(s3UploadUrl); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Upload failed: No S3 URL returned')), + ); } - - // Start Playback - await _player!.startPlayer( - fromURI: _pathToSaveRecording, - codec: Codec.pcm16WAV, // Make sure this matches the recording codec - whenFinished: () { - setState(() { - _isPlaying = false; - }); - _player!.closePlayer(); // Close player when finished - }, - ); - - setState(() { - _isPlaying = true; - }); - - print("🎵 Playback started: $_pathToSaveRecording"); - } catch (e) { - print("🚨 Error during playback: $e"); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Playback failed: $e')), - ); } } - // Function to handle stopping the playback of the recorded audio. - Future _stopPlayback() async { - await _player!.stopPlayer(); - setState(() { - _isPlaying = false; - }); - _player!.closePlayer(); - } - Future _transcribeAudio(String s3Url) async { - // Ensure AWS credentials are properly configured try { + if (_recordingKey == null) return; + String s3Uri = "s3://$_bucketName/audio/${s3Url.split('/').last}"; - print(s3Uri); - // Starting the transcription job - final response = await service.startTranscriptionJob( - transcriptionJobName: '${key2}transcript', + await service.startTranscriptionJob( + transcriptionJobName: '${_recordingKey}transcript', media: trans.Media(mediaFileUri: s3Uri), mediaFormat: trans.MediaFormat.wav, languageCode: trans.LanguageCode.enUs, - settings: trans.Settings( - showSpeakerLabels: true, - maxSpeakerLabels: - 2, // specify the number of speakers you expect, adjust as needed - ), ); + setState(() { _isTranscribing = true; + _currentStatus = "Transcribing..."; }); - print( - 'Transcription job started with status: ${response.transcriptionJob?.transcriptionJobStatus}'); - // Poll for the transcription job's status while (true) { final jobResponse = await service.getTranscriptionJob( - transcriptionJobName: '${key2}transcript', + transcriptionJobName: '${_recordingKey}transcript', ); + if (jobResponse.transcriptionJob?.transcriptionJobStatus.toString() == 'TranscriptionJobStatus.completed') { final transcriptUri = jobResponse.transcriptionJob?.transcript?.transcriptFileUri; + if (transcriptUri != null) { final transcriptResponse = await http.get(Uri.parse(transcriptUri)); + if (transcriptResponse.statusCode == 200) { var jsonResponse = jsonDecode(transcriptResponse.body); var items = jsonResponse['results']['items']; - - // Construct transcription text with speaker labels - // Construct transcription text with speaker labels and start on a new line for each speaker var fullTranscription = ''; String? currentSpeaker; for (var item in items) { - // Check for speaker label - if (item['type'] == 'pronunciation' && - item.containsKey('speaker_label')) { - String speakerLabel = - _getCustomSpeakerLabel(item['speaker_label']); - if (currentSpeaker != speakerLabel) { - fullTranscription += '\n$speakerLabel: '; - currentSpeaker = speakerLabel; + if (item['type'] == 'pronunciation') { + // Check if speaker label is available + String? speakerLabel = item['speaker_label']; + + // Add speaker header if it changed + if (speakerLabel != null) { + String label = _getCustomSpeakerLabel(speakerLabel); + if (currentSpeaker != label) { + fullTranscription += '\n$label: '; + currentSpeaker = label; + } + } else if (currentSpeaker == null) { + // Default to Speaker 1 if none provided + currentSpeaker = 'Speaker 1'; + fullTranscription += '\n$currentSpeaker: '; } + fullTranscription += item['alternatives'][0]['content'] + ' '; } else if (item['type'] == 'punctuation') { fullTranscription = fullTranscription.trim() + @@ -324,73 +225,84 @@ class _AudioScreenState extends State { ' '; } } + + if (fullTranscription.trim().isEmpty) { + fullTranscription = + "[No transcription detected — was the audio too short or unclear?]"; + } + setState(() { transcription = fullTranscription.trim(); _isTranscribing = false; + _currentStatus = "Transcription Complete"; }); - } else { - print( - 'Failed to fetch transcript: ${transcriptResponse.statusCode}'); - _isTranscribing = false; + + await _saveTranscriptionToFile('${_recordingKey}transcript'); + + // Wait a moment to make sure filesystem is caught up + await Future.delayed(Duration(milliseconds: 300)); + + final directory = await getApplicationDocumentsDirectory(); + final transcriptFile = File( + '${directory.path}/files/audios/transcripts/${_recordingKey}transcript.txt', + ); + + if (await transcriptFile.exists()) { + print("Transcript file found. Starting summarization..."); + transcriptionSummary = + await summarizeFileContent('${_recordingKey}transcript'); + await _saveTranscriptionSummaryToFile(_recordingKey!); + } else { + print("Transcript file not found. Skipping summarization."); + } } - break; } - } else if (jobResponse.transcriptionJob?.transcriptionJobStatus - .toString() == - 'TranscriptionJobStatus.failed') { - print('Transcription job failed'); - _isTranscribing = false; + break; } - // Wait for a short interval before polling again + await Future.delayed(Duration(seconds: 2)); } } catch (e) { - print('Error starting transcription: $e'); + print('Error during transcription: $e'); } - _saveTranscriptionToFile('${key2}transcript'); } String _getCustomSpeakerLabel(String awsSpeakerLabel) { - if (awsSpeakerLabel == 'spk_0') { - return 'Speaker 1'; - } else if (awsSpeakerLabel == 'spk_1') { - return 'Speaker 2'; - } else if (awsSpeakerLabel == 'spk_2') { - return 'Speaker 3'; - } else if (awsSpeakerLabel == 'spk_3') { - return 'Speaker 4'; - } else { - return awsSpeakerLabel; + switch (awsSpeakerLabel) { + case 'spk_0': + return 'Speaker 1'; + case 'spk_1': + return 'Speaker 2'; + case 'spk_2': + return 'Speaker 3'; + case 'spk_3': + return 'Speaker 4'; + default: + return awsSpeakerLabel; } } - Future _saveTranscriptionToFile(String transcriptionJobName) async { - if (transcription.isEmpty) { - print("Transcription is empty. Nothing to save."); - setState(() { - _isTranscribing = false; - }); - return; - } + Future _saveTranscriptionToFile(String transcriptionJobName) async { + if (transcription.isEmpty) return; try { Directory appDocDirectory = await getApplicationDocumentsDirectory(); - String filePath = - '${appDocDirectory.path}/files/audios/transcripts/$transcriptionJobName.txt'; + String dirPath = '${appDocDirectory.path}/files/audios/transcripts'; + String filePath = '$dirPath/$transcriptionJobName.txt'; + + // Make sure the folder exists + await Directory(dirPath).create(recursive: true); File file = File(filePath); await file.writeAsString(transcription); print("Transcription saved at $filePath"); - transcriptionSummary = await summarizeFileContent(transcriptionJobName); - _saveTranscriptionSummaryToFile(transcriptionJobName); } catch (e) { - print("Error saving transcription"); + print("Error saving transcription: $e"); } } - // Save transcription summary Future _saveTranscriptionSummaryToFile( String transcriptionSummaryName) async { if (transcriptionSummary.isEmpty) { @@ -407,22 +319,21 @@ class _AudioScreenState extends State { await file.writeAsString(transcriptionSummary); print("Transcription Summary saved at $filePath"); - _sendToDatabase(); + await _sendToDatabase(); } catch (e) { - print("Error saving transcription"); + print("Error saving transcription summary: $e"); } } Future summarizeFileContent(String fileName) async { try { - // Read file content final directory = await getApplicationDocumentsDirectory(); final file = File('${directory.path}/files/audios/transcripts/$fileName.txt'); String content = await file.readAsString(); print("Sending to OpenAI for summarization"); - // Send to OpenAI for Summarization + final response = await http.post( Uri.parse(API_URL), headers: { @@ -435,20 +346,15 @@ class _AudioScreenState extends State { 'role': 'user', 'content': 'Does this content contain sensitive personal information?: $content' - } // Your actual request + } ], - 'max_tokens': 500, // Adjust this as we need to + 'max_tokens': 500, 'model': 'gpt-4', }), ); + if (response.statusCode == 200) { var jsonResponse = jsonDecode(response.body); - - if (jsonResponse['choices'] == null || - jsonResponse['choices'].isEmpty) { - throw Exception("No choices return from OpenAI."); - } - var summary = jsonResponse['choices'][0]['message']['content']?.trim() ?? ""; @@ -474,301 +380,146 @@ class _AudioScreenState extends State { } } - // Function to send Firebase notification Future sendFirebaseNotification(String title, String body) async { - FirebaseMessaging messaging = FirebaseMessaging.instance; + print("Sending notification to caregiver via Firebase Function..."); try { - String? token = await messaging.getToken(); - - if (token == null) { - print("Firebase token is null. Cannot send notfication."); - return; - } + final url = Uri.parse( + 'https://us-central1-spring2025-81f5b.cloudfunctions.net/sendNotification', + ); final response = await http.post( - Uri.parse("https://fcm.googleapis.com/fcm/send"), + url, headers: { - 'Content-Type': 'application/json', - 'Authorization': 'key=${dotenv.env['GOOGLE_CLOUD_API']}' + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: { + 'topic': 'Sensitive Information Detected', // or 'caregivers', etc. + 'title': title, + 'message': body, }, - body: jsonEncode({ - "to": token, - "notification": {"title": title, "body": body} - }), ); if (response.statusCode == 200) { - print("Firebase Notification Sent!"); + print("Notification sent successfully via function!"); } else { - print("Failed to send notification: ${response.body}"); + print("Failed to send notification. Response: ${response.body}"); } } catch (e) { - print("Error sending Firebase notification: $e"); + print("Error sending Firebase notification via function: $e"); } } Future _sendToDatabase() async { - // Call add method to db - Directory appDocDirectory = await getApplicationDocumentsDirectory(); - String audioFilePath = '${appDocDirectory.path}/files/audios/$key2.wav'; - String transcriptFilePath = - '${appDocDirectory.path}/files/audios/transcripts/${key2}transcript.txt'; - final dateTime = DateTime.fromMillisecondsSinceEpoch(int.parse(key2)); - final dateFormat = DateFormat('MM/dd/yyyy'); - final title = dateFormat.format(dateTime); - audio = await DataService.instance.addAudio( + try { + final appDocDirectory = await getApplicationDocumentsDirectory(); + String audioFilePath = + '${appDocDirectory.path}/files/audios/$_recordingKey.wav'; + String transcriptFilePath = + '${appDocDirectory.path}/files/audios/transcripts/${_recordingKey}transcript.txt'; + + final dateTime = + DateTime.fromMillisecondsSinceEpoch(int.parse(_recordingKey!)); + final dateFormat = DateFormat('MM/dd/yyyy'); + final title = dateFormat.format(dateTime); + + audio = await DataService.instance.addAudio( title: title, description: "", audioFile: File(audioFilePath), transcriptFile: File(transcriptFilePath), - summary: transcriptionSummary); - audioId = audio?.id; - print(audioId); + summary: transcriptionSummary, + ); + + audioId = audio?.id; + print("Audio saved with ID: $audioId"); + } catch (e) { + print("Error saving to database: $e"); + } } @override Widget build(BuildContext context) { return Scaffold( - extendBodyBehindAppBar: true, - extendBody: true, - appBar: AppBar( - backgroundColor: const Color(0x440000), - elevation: 0, - centerTitle: true, - leading: const BackButton(color: Colors.black54), - title: const Text('Audio Recording', - style: TextStyle(color: Colors.black54)), - ), - body: Container( - decoration: const BoxDecoration(), - child: Column( - children: [ - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 20), - if (_isRecording) - Column( - children: [ - AvatarGlow( - glowColor: Colors.red, - glowRadiusFactor: 100.0, - duration: Duration(milliseconds: 2000), - repeat: true, - animate: true, - startDelay: Duration(milliseconds: 100), - child: Material( - // Replace this child with your own - elevation: 8.0, - shape: CircleBorder(), - child: CircleAvatar( - backgroundColor: Colors.grey[100], - radius: 70.0, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - TextButton( - style: ButtonStyle( - shape: MaterialStateProperty.all< - RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(75.0), - ))), - onPressed: () async { - await _stopRecording(); - }, - child: const Column( - mainAxisAlignment: - MainAxisAlignment.center, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - Icon(Icons.stop, - size: 65, color: Colors.red), - Text( - "Stop Audio Recording", - textAlign: TextAlign.center, - ) - ], - ), - ), - ], - ), - ), - ), - ), - Text( - _duration - .toString() - .split('.') - .first - .padLeft(8, "0"), - style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold), - ), - ], - ) - else if (_pathToSaveRecording != null) - Expanded( - child: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only(top: 200.0), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - icon: _isPlaying - ? Icon(Icons.pause, - size: 40, - color: Color.fromARGB( - 255, 2, 63, 129)) - : Icon(Icons.play_arrow, - size: 40, - color: Color.fromARGB( - 255, 2, 63, 129)), - onPressed: _isPlaying - ? _startPlayback - : _startPlayback, - ), - IconButton( - icon: Icon(Icons.stop, - size: 40, - color: Color.fromARGB( - 255, 2, 63, 129)), - onPressed: _isPlaying || _isPaused - ? _stopPlayback - : null, - ), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - onPressed: () { - setState(() { - _pathToSaveRecording = null; - _duration = - const Duration(seconds: 0); - transcription = ''; - }); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Color.fromARGB(255, - 2, 63, 129), // Background color - foregroundColor: - Colors.white, // Text color - ), - child: const Text('New Recording'), - ), - SizedBox(width: 32), - ElevatedButton( - onPressed: () async { - // Your logic to remove audio - if (audioId != null) { - await DataService.instance - .removeAudio(audioId!); - } - setState(() { - _pathToSaveRecording = null; - _duration = - const Duration(seconds: 0); - transcription = ''; - }); - // Notify user that the recording has been deleted - ScaffoldMessenger.of(context) - .showSnackBar( - SnackBar( - content: const Text( - 'Recording Deleted!', - textAlign: TextAlign - .center, // Centers the text in the SnackBar - ), - backgroundColor: Colors - .red, // Optional: Makes SnackBar red - ), - ); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors - .red, // background (button) color - adjust as needed - foregroundColor: Colors - .white, // foreground (text/icon) color - adjust as needed - ), - child: Icon(Icons - .delete), // Use the `delete` icon - ) - ], - ), - if (_isTranscribing) TypingIndicator(), - Padding( - padding: const EdgeInsets.all(15.0), - child: Text( - transcription, - style: const TextStyle(fontSize: 16), - ), - ), - ], - ), - ), - ), - ) - else - AvatarGlow( - glowColor: Colors.blue, - glowRadiusFactor: 100.0, - duration: Duration(milliseconds: 2000), - repeat: true, - animate: true, - startDelay: Duration(milliseconds: 100), - child: Material( - // Replace this child with your own - elevation: 8.0, - shape: CircleBorder(), - child: CircleAvatar( - backgroundColor: const Color(0xFFFFFFFF), - radius: 70.0, - child: TextButton( - onPressed: _startRecording, - style: ButtonStyle( - shape: MaterialStateProperty.all< - RoundedRectangleBorder>( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(75.0), - ))), - child: const Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(Icons.mic, - size: 60, - color: Color.fromARGB(255, 2, 63, 129)), - Text( - "Start Audio Recording", - textAlign: TextAlign.center, - ) - ], - ), - ), - ), - ), + appBar: AppBar( + backgroundColor: const Color(0x440000), + elevation: 0, + centerTitle: true, + leading: const BackButton(color: Colors.black54), + title: const Text('Audio Recording', + style: TextStyle(color: Colors.black54)), + ), + body: Column( + children: [ + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_isRecording) + AvatarGlow( + glowColor: Colors.red, + glowRadiusFactor: 100.0, + duration: Duration(milliseconds: 2000), + repeat: true, + animate: true, + startDelay: Duration(milliseconds: 100), + child: Material( + elevation: 8.0, + shape: CircleBorder(), + child: CircleAvatar( + backgroundColor: Colors.grey[100], + radius: 70.0, + child: IconButton( + onPressed: _stopRecording, + icon: Icon(Icons.stop, size: 65, color: Colors.red), + ), + ), + ), + ) + else + AvatarGlow( + glowColor: Colors.blue, + glowRadiusFactor: 100.0, + duration: Duration(milliseconds: 2000), + repeat: true, + animate: true, + startDelay: Duration(milliseconds: 100), + child: Material( + elevation: 8.0, + shape: CircleBorder(), + child: CircleAvatar( + backgroundColor: Colors.white, + radius: 70.0, + child: IconButton( + onPressed: _startRecording, + icon: Icon(Icons.mic, size: 60, color: Colors.blue), ), - ], + ), + ), ), + SizedBox(height: 20), + Text( + _currentStatus, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), - ), - ], + SizedBox(height: 20), + Container( + padding: EdgeInsets.all(16), + margin: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(10), + ), + child: transcription.isEmpty + ? Text('Transcription will appear here...') + : Text(transcription, style: TextStyle(fontSize: 16)), + ), + ], + ), ), - ), - bottomNavigationBar: UiUtils.createBottomNavigationBar(context)); + ], + ), + bottomNavigationBar: UiUtils.createBottomNavigationBar(context), + ); } } 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 c20ffade..a292feed 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 @@ -1,13 +1,18 @@ +//lib/src/features/stml_user_dashboard/presentation/stml_user_dashboard.dart // ignore_for_file: avoid_print, prefer_const_constructors -// Imported libraries and packages +import 'package:memoryminder/services/notification_service.dart'; import 'package:memoryminder/src/features/caregiver-dashboard/presentation/app_bar.dart'; -import 'package:memoryminder/src/features/help/help_screen.dart'; +import 'package:memoryminder/src/features/caregiver-dashboard/presentation/caregiver-dashboard.dart'; +import 'package:memoryminder/src/features/dementia-resources/dementia_resources.dart'; +import 'package:memoryminder/ui/help_screen.dart'; import 'package:memoryminder/ui/response_screen.dart'; +import 'package:memoryminder/ui/assistant_screen.dart'; import 'package:memoryminder/src/features/sensitive_information_detection/presentation/audio_screen.dart'; import 'package:memoryminder/ui/gallery_screen.dart'; import 'package:memoryminder/ui/profile_screen.dart'; import 'package:memoryminder/ui/scam_detection_screen.dart'; +import 'package:memoryminder/ui/tour_screen.dart'; import 'package:memoryminder/ui/location_history_screen.dart'; import 'package:geocoding/geocoding.dart'; import 'package:geolocator/geolocator.dart'; @@ -15,20 +20,19 @@ 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/ReturnMeHome.dart'; +import 'package:memoryminder/src/features/wearable-integration/fitbit_login.dart'; - -// Main HomeScreen widget which is a stateless widget. class STMLUserDashboardScreen extends StatefulWidget { + const STMLUserDashboardScreen({super.key}); + @override - _STMLUserDashboardScreenState createState() => _STMLUserDashboardScreenState(); + _STMLUserDashboardScreenState createState() => + _STMLUserDashboardScreenState(); } class _STMLUserDashboardScreenState extends State { bool hasBeenInitialized = false; double iconSize = 65; - - // To keep track of the current location LocationEntry? currentLocationEntry; @override @@ -38,7 +42,7 @@ class _STMLUserDashboardScreenState extends State { _listenToLocationChanges(); } - _initializeCamera() { + void _initializeCamera() { if (!hasBeenInitialized) { CameraManager cm = CameraManager(); cm.startAutoRecording(); @@ -46,7 +50,7 @@ class _STMLUserDashboardScreenState extends State { } } - _listenToLocationChanges() { + void _listenToLocationChanges() { final locationStream = Geolocator.getPositionStream(); locationStream.listen((Position position) async { try { @@ -82,22 +86,18 @@ class _STMLUserDashboardScreenState extends State { @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', + title: 'STML User 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!', + 'Helping you remember the important things.\nChoose a feature to get started!', style: TextStyle( fontSize: 16.0, color: Colors.black54, @@ -105,8 +105,6 @@ class _STMLUserDashboardScreenState extends State { textAlign: TextAlign.center, ), ), - // Grid view to display multiple options/buttons - Expanded( child: GridView.count( physics: const NeverScrollableScrollPhysics(), @@ -116,25 +114,23 @@ class _STMLUserDashboardScreenState extends State { childAspectRatio: 1.30, padding: const EdgeInsets.all(26.0), children: [ - // Using the helper function to build each button in the grid _buildElevatedButton( context: context, icon: Icon(Icons.home_filled, size: iconSize, color: Colors.black54), text: 'Take Me Home', - screen: ReturnMeHomePage(), + screen: ProfileScreen(), keyName: "TakeMeHomeButtonKey", backgroundColor: const Color(0xFF000000).withOpacity(0.30)), _buildElevatedButton( context: context, - icon: Icon(Icons.sos_sharp, - size: iconSize, color: Colors.black54), + icon: Icon(Icons.help_outline, + size: iconSize, color: Colors.white), text: 'HELP', screen: HelpScreen(), keyName: "HelpButtonKey", - backgroundColor: const Color(0xFFFFFFFF).withOpacity(0.30), - ), + backgroundColor: Colors.red.withOpacity(0.80)), _buildElevatedButton( context: context, icon: Icon(Icons.photo, @@ -195,13 +191,9 @@ class _STMLUserDashboardScreenState extends State { ], ), ), - - // Bottom navigation bar with multiple options for quick navigation bottomNavigationBar: UiUtils.createBottomNavigationBar(context)); } - - // Helper function to create each button for the GridView Widget _buildElevatedButton({ required BuildContext context, required Icon icon, @@ -224,13 +216,11 @@ class _STMLUserDashboardScreenState extends State { ), onPressed: () { if (routeName != null) { - Navigator.pushNamed(context, routeName); // Use named route if provided - } - else if (screen != 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/utils/ui_utils.dart b/STML/stml_application/lib/src/utils/ui_utils.dart index 04e131f3..df7a4d09 100644 --- a/STML/stml_application/lib/src/utils/ui_utils.dart +++ b/STML/stml_application/lib/src/utils/ui_utils.dart @@ -47,7 +47,9 @@ class UiUtils { if (index == 0) { // Navigate to Gallery screen Navigator.push( - context, MaterialPageRoute(builder: (context) => STMLUserDashboardScreen())); + context, + MaterialPageRoute( + builder: (context) => STMLUserDashboardScreen())); } else if (index == 1) { // Navigate to Search screen Navigator.push( diff --git a/STML/stml_application/lib/ui/help_screen.dart b/STML/stml_application/lib/ui/help_screen.dart new file mode 100644 index 00000000..aae3e7d4 --- /dev/null +++ b/STML/stml_application/lib/ui/help_screen.dart @@ -0,0 +1,330 @@ +//help_screen.dart +import 'package:flutter/material.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:memoryminder/services/notification_service.dart'; +import 'package:memoryminder/ui/location_history_screen.dart'; +import 'package:memoryminder/src/utils/permission_manager.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'dart:async'; // For StreamSubscription + +class HelpScreen extends StatefulWidget { + const HelpScreen({Key? key}) : super(key: key); + + @override + State createState() => _HelpScreenState(); +} + +class _HelpScreenState extends State { + final NotificationService _notificationService = NotificationService(); + bool _isLoading = false; + bool _helpSent = false; + String? _requestId; + String _statusMessage = ''; + StreamSubscription? _statusSubscription; + + @override + void initState() { + super.initState(); + _notificationService.initialize(); + _checkPermissions(); + + // Log screen view for analytics + FirebaseAnalytics.instance.logScreenView( + screenName: 'help_screen', + ); + } + + @override + void dispose() { + _statusSubscription?.cancel(); + super.dispose(); + } + + Future _checkPermissions() async { + // Check general permissions using the existing PermissionManager + await PermissionManager.requestInitialPermissions(); + + // Additionally check location service + await PermissionManager.checkIfLocationServiceIsActive(context); + + // Also request notification permission (not covered in PermissionManager) + await _requestNotificationPermission(); + } + + Future _requestNotificationPermission() async { + NotificationSettings settings = + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + provisional: false, + ); + + if (settings.authorizationStatus == AuthorizationStatus.denied) { + if (mounted) { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Notifications Required"), + content: const Text( + "Notifications are essential for caregiver alerts. Please enable them in your device settings."), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("OK"), + ), + ], + ), + ); + } + return false; + } + + return settings.authorizationStatus == AuthorizationStatus.authorized; + } + + void _subscribeToHelpRequestUpdates(String requestId) { + _statusSubscription?.cancel(); + + _statusSubscription = FirebaseFirestore.instance + .collection('helpRequests') + .doc(requestId) + .snapshots() + .listen((snapshot) { + if (!mounted) return; + + if (snapshot.exists) { + final status = snapshot.data()?['status']; + + setState(() { + if (status == 'responded') { + _statusMessage = 'Your caregiver has confirmed and is on the way!'; + + // Log status update received + FirebaseAnalytics.instance.logEvent( + name: 'help_request_status_updated', + parameters: { + 'request_id': requestId, + 'status': status, + }, + ); + + // Show a prominent notification + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Your caregiver is on the way!'), + backgroundColor: Colors.green, + duration: Duration(seconds: 10), + ), + ); + } else { + _statusMessage = + 'Help request sent. Waiting for caregiver to respond...'; + } + }); + } + }); + } + + Future _sendHelpRequest() async { + // Check permissions before sending alert + bool hasGeneralPermissions = + await PermissionManager.requestInitialPermissions(); + bool hasLocationService = + await PermissionManager.checkIfLocationServiceIsActive(context); + bool hasNotificationPermission = await _requestNotificationPermission(); + + if (!hasGeneralPermissions || + !hasLocationService || + !hasNotificationPermission) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Required permissions are missing'), + backgroundColor: Colors.orange, + ), + ); + return; + } + + setState(() { + _isLoading = true; + _helpSent = false; + _statusMessage = ''; + }); + + try { + // Get the most recent location entry + final locations = await LocationDatabase.instance.readAllLocations(); + final currentLocation = locations.isNotEmpty ? locations.first : null; + + // Send the help notification with location data + final success = + await _notificationService.sendHelpNotification(currentLocation); + + // Get the request ID from the notification service + _requestId = _notificationService.lastRequestId; + + if (!mounted) return; + + if (success && _requestId != null) { + setState(() { + _helpSent = true; + _statusMessage = + 'Help request sent. Waiting for caregiver to respond...'; + }); + + // Subscribe to updates on this request + _subscribeToHelpRequestUpdates(_requestId!); + + // Show feedback + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Help request sent successfully to caregiver.'), + backgroundColor: Colors.green, + ), + ); + } else { + // Show failure message + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to send help request. Please try again.'), + backgroundColor: Colors.red, + ), + ); + } + } catch (e) { + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + Widget _buildStatusIndicator() { + if (!_helpSent) return const SizedBox.shrink(); + + return Container( + margin: const EdgeInsets.symmetric(vertical: 20, horizontal: 24), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.blue[50], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.blue[200]!), + ), + child: Column( + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.blue[100], + shape: BoxShape.circle, + ), + child: const Icon(Icons.info_outline, color: Colors.blue), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _statusMessage, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + const LinearProgressIndicator(), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Help'), + ), + body: Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.emergency, + size: 80, + color: Colors.red, + ), + const SizedBox(height: 20), + const Text( + 'Need assistance?', + style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + + // Status indicator (shows when help is sent) + _buildStatusIndicator(), + + const SizedBox(height: 20), + _isLoading + ? const Column( + children: [ + CircularProgressIndicator(color: Colors.red), + SizedBox(height: 10), + Text( + 'Sending emergency alert...', + style: TextStyle(color: Colors.red), + ), + ], + ) + : _helpSent + ? ElevatedButton.icon( + icon: const Icon(Icons.refresh), + label: const Text('SEND ANOTHER ALERT'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 15), + ), + onPressed: _sendHelpRequest, + ) + : ElevatedButton.icon( + icon: const Icon(Icons.warning_amber_rounded), + label: const Text('SEND EMERGENCY ALERT'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + padding: const EdgeInsets.symmetric( + horizontal: 40, vertical: 15), + ), + onPressed: _sendHelpRequest, + ), + const SizedBox(height: 20), + const Text( + 'This will send your current location to your caregiver', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + + if (_helpSent) const SizedBox(height: 40), + ], + ), + ), + ), + ); + } +} diff --git a/STML/stml_application/lib/ui/stml_calendar_screen.dart b/STML/stml_application/lib/ui/stml_calendar_screen.dart new file mode 100644 index 00000000..d1bae683 --- /dev/null +++ b/STML/stml_application/lib/ui/stml_calendar_screen.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:memoryminder/src/features/caregiver-dashboard/presentation/app_bar.dart'; +import 'package:syncfusion_flutter_calendar/calendar.dart'; + +class stmlCalendarScreen extends StatelessWidget { + const stmlCalendarScreen({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + extendBody: true, + // Setting up the app bar at the top of the screen + appBar: const CustomAppBar( + title: 'Recipient Profile', + ), + body: SfCalendar( + view: CalendarView.month, + allowedViews: + [ + CalendarView.day, + CalendarView.week, + CalendarView.workWeek, + CalendarView.month, + CalendarView.schedule + ], + headerHeight: 100, + showDatePickerButton: true, + showTodayButton: true, + allowViewNavigation: true, + monthViewSettings: MonthViewSettings( + appointmentDisplayMode: MonthAppointmentDisplayMode.appointment, + showAgenda: true, + agendaViewHeight: 400, + ), + ), + floatingActionButton: FloatingActionButton( + backgroundColor: Colors.yellow, + onPressed: () { + Navigator.pop(context); + }, + child: Icon(Icons.thumb_up, color: Colors.black), + ), + ), + ); + } +} \ No newline at end of file diff --git a/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift b/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift index 20fb3502..a57bdfeb 100644 --- a/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/STML/stml_application/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import cloud_firestore +import cloud_functions import file_selector_macos +import firebase_analytics import firebase_auth import firebase_core import firebase_messaging @@ -26,7 +28,9 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFunctionsPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) diff --git a/STML/stml_application/pubspec.yaml b/STML/stml_application/pubspec.yaml index 279902fd..d8c221b3 100644 --- a/STML/stml_application/pubspec.yaml +++ b/STML/stml_application/pubspec.yaml @@ -69,6 +69,8 @@ dependencies: flutter_secure_storage: ^8.0.0 http: ^0.13.6 fl_chart: ^0.64.0 + cloud_functions: ^5.3.4 + firebase_analytics: ^11.4.4 dev_dependencies: flutter_test: