Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .gitignore
Binary file not shown.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cmake.sourceDirectory": "C:/Users/ludov/Desktop/UMGC stml/spring2025/STML/stml_application/linux"
}
4 changes: 4 additions & 0 deletions STML/stml_application/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSMicrophoneUsageDescription</key>
<string>We need microphone access to record audio for memory notes.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>This app needs access to save and read audio transcripts.</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
Expand Down
250 changes: 250 additions & 0 deletions STML/stml_application/lib/services/notification_service.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<void> _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<bool> sendHelpNotification(LocationEntry? currentLocationEntry) async {
try {
// Start timer for performance tracking
final startTime = DateTime.now();

// Prepare location data
Map<String, dynamic>? 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<Location> 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<String, Object> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -34,7 +35,7 @@ class _LoginScreenState extends State<LoginScreen> {
if (didAuthenticate) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => HomeScreen()),
MaterialPageRoute(builder: (context) => STMLUserDashboardScreen()),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
Expand All @@ -50,7 +51,6 @@ class _LoginScreenState extends State<LoginScreen> {
}

Future<void> _loginWithEmail() async {

setState(() {
_isAuthenticating = true;
});
Expand Down Expand Up @@ -80,7 +80,6 @@ class _LoginScreenState extends State<LoginScreen> {
PermissionManager.checkIfLocationServiceIsActive(context);
return Scaffold(
body: Container(

child: Center(
child: Padding(
padding:
Expand Down Expand Up @@ -111,15 +110,14 @@ class _LoginScreenState extends State<LoginScreen> {
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,
Expand All @@ -145,10 +143,16 @@ class _LoginScreenState extends State<LoginScreen> {
? 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),
),
Expand All @@ -160,11 +164,18 @@ class _LoginScreenState extends State<LoginScreen> {

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),
),
Expand All @@ -176,7 +187,7 @@ class _LoginScreenState extends State<LoginScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RegistrationScreen()),
builder: (context) => EulaScreen()),
);
},
child: Row(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0XFF880E4F),
extendBodyBehindAppBar: true,
extendBody: true,
appBar: AppBar(
Expand All @@ -97,12 +96,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
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:
Expand Down
Loading