From f36643baec2036a5dda2396f0ea12a319a4763f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSagargit=20config=20--global=20user=2Eemail=20?= =?UTF-8?q?=E2=80=9Cmsagar2606=40gmail=2Ecom?= Date: Fri, 29 Nov 2024 14:55:45 -0700 Subject: [PATCH 1/6] send notifs to entrants based on winning or losing implemented. need to implement notification preference for entrants and add a notificationsEnabled field in user_profile --- app/src/main/AndroidManifest.xml | 7 +- .../NotificationActionReceiver.java | 196 ++++++-- .../vortex_app/model/NotificationModel.java | 14 +- .../com/example/vortex_app/model/User.java | 18 +- .../notification/NotificationsActivity.java | 457 +++++++++++++++++- .../view/profile/ProfileActivity.java | 1 + .../waitinglist/OrgWaitingListActivity.java | 128 ++++- .../res/layout/activity_notifications.xml | 40 +- 8 files changed, 781 insertions(+), 80 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 889dcb7..1d0ea4e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,8 @@ + + - + - + This receiver allows the app to perform actions in response to user interactions with notifications - * without needing to open an activity. + * It updates the entrant's status in Firestore based on the user's response. */ public class NotificationActionReceiver extends BroadcastReceiver { private FirebaseFirestore db; - /** - * Called when the receiver receives a broadcasted intent. It determines the action taken - * by the user and calls the appropriate method to handle the action. - * - * @param context The {@link Context} in which the receiver is running. - * @param intent The {@link Intent} being received, containing the action and invitation ID. - */ + // Define action strings (should match those in NotificationsActivity) + public static final String ACTION_ACCEPT = "ACTION_ACCEPT"; + public static final String ACTION_DECLINE = "ACTION_DECLINE"; + public static final String ACTION_STAY = "ACTION_STAY"; + public static final String ACTION_LEAVE = "ACTION_LEAVE"; + @Override public void onReceive(Context context, Intent intent) { db = FirebaseFirestore.getInstance(); // Initialize Firestore String action = intent.getAction(); - String invitationId = intent.getStringExtra("invitationId"); // Retrieve the invitation ID + String userID = intent.getStringExtra("userID"); + String eventID = intent.getStringExtra("eventID"); + + if (userID == null || eventID == null) { + Log.e("NotificationActionReceiver", "Missing userID or eventID."); + return; + } switch (action) { - case "ACTION_ACCEPT": - handleAccept(context, invitationId); + case ACTION_ACCEPT: + handleAccept(context, userID, eventID); break; - case "ACTION_DECLINE": - handleDecline(context, invitationId); + case ACTION_DECLINE: + handleDecline(context, userID, eventID); break; - case "ACTION_STAY": - handleStay(context); + case ACTION_STAY: + handleStay(context, userID, eventID); break; - case "ACTION_LEAVE": - handleLeave(context, invitationId); + case ACTION_LEAVE: + handleLeave(context, userID, eventID); break; default: + Log.w("NotificationActionReceiver", "Unknown action received: " + action); break; } } /** - * Handles the "Accept" action for an invitation. Updates the invitation status in Firestore to "accepted". + * Handles the "Accept" action. Moves the entrant from 'selected' to 'enrolled'. * - * @param context The {@link Context} in which the receiver is running. - * @param invitationId The ID of the invitation to be updated. + * @param context The {@link Context} in which the receiver is running. + * @param userID The ID of the user. + * @param eventID The ID of the event. */ - private void handleAccept(Context context, String invitationId) { - db.collection("invitations").document(invitationId) - .update("status", "accepted") - .addOnSuccessListener(aVoid -> { - Toast.makeText(context, "Invitation Accepted", Toast.LENGTH_SHORT).show(); + private void handleAccept(Context context, String userID, String eventID) { + // Query to find the user's document in 'selected' collection + db.collection("selected") + .whereEqualTo("userID", userID) + .whereEqualTo("eventID", eventID) + .get() + .addOnSuccessListener(querySnapshot -> { + if (!querySnapshot.isEmpty()) { + DocumentSnapshot selectedDoc = querySnapshot.getDocuments().get(0); + DocumentReference selectedRef = selectedDoc.getReference(); + DocumentReference enrolledRef = db.collection("enrolled").document(); + + WriteBatch batch = db.batch(); + batch.set(enrolledRef, selectedDoc.getData()); + batch.delete(selectedRef); + + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(context, "You have been enrolled!", Toast.LENGTH_SHORT).show(); + Log.d("NotificationActionReceiver", "User moved to 'enrolled'."); + }) + .addOnFailureListener(e -> { + Toast.makeText(context, "Error enrolling in the event.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Transaction failed: " + e.getMessage()); + }); + } else { + Toast.makeText(context, "Selected document not found.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Selected document not found for userID: " + userID + ", eventID: " + eventID); + } }) .addOnFailureListener(e -> { - Toast.makeText(context, "Error accepting invitation", Toast.LENGTH_SHORT).show(); + Toast.makeText(context, "Error fetching selected document.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Error fetching selected document: ", e); }); } /** - * Handles the "Decline" action for an invitation. Updates the invitation status in Firestore to "declined". + * Handles the "Decline" action. Moves the entrant from 'selected' to 'cancelled'. * - * @param context The {@link Context} in which the receiver is running. - * @param invitationId The ID of the invitation to be updated. + * @param context The {@link Context} in which the receiver is running. + * @param userID The ID of the user. + * @param eventID The ID of the event. */ - private void handleDecline(Context context, String invitationId) { - db.collection("invitations").document(invitationId) - .update("status", "declined") - .addOnSuccessListener(aVoid -> { - Toast.makeText(context, "Invitation Declined", Toast.LENGTH_SHORT).show(); + private void handleDecline(Context context, String userID, String eventID) { + // Query to find the user's document in 'selected' collection + db.collection("selected") + .whereEqualTo("userID", userID) + .whereEqualTo("eventID", eventID) + .get() + .addOnSuccessListener(querySnapshot -> { + if (!querySnapshot.isEmpty()) { + DocumentSnapshot selectedDoc = querySnapshot.getDocuments().get(0); + DocumentReference selectedRef = selectedDoc.getReference(); + DocumentReference cancelledRef = db.collection("cancelled").document(); + + WriteBatch batch = db.batch(); + batch.set(cancelledRef, selectedDoc.getData()); + batch.delete(selectedRef); + + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(context, "You have declined the invitation.", Toast.LENGTH_SHORT).show(); + Log.d("NotificationActionReceiver", "User moved to 'cancelled'."); + }) + .addOnFailureListener(e -> { + Toast.makeText(context, "Error declining the invitation.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Transaction failed: " + e.getMessage()); + }); + } else { + Toast.makeText(context, "Selected document not found.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Selected document not found for userID: " + userID + ", eventID: " + eventID); + } }) .addOnFailureListener(e -> { - Toast.makeText(context, "Error declining invitation", Toast.LENGTH_SHORT).show(); + Toast.makeText(context, "Error fetching selected document.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Error fetching selected document: ", e); }); } /** - * Handles the "Stay" action, which keeps the user on the waiting list. No Firestore update is required; - * this method simply shows a confirmation message to the user. + * Handles the "Stay" action. Keeps the entrant in the 'waitlisted' collection. * - * @param context The {@link Context} in which the receiver is running. + * @param context The {@link Context} in which the receiver is running. + * @param userID The ID of the user. + * @param eventID The ID of the event. */ - private void handleStay(Context context) { - Toast.makeText(context, "You have chosen to stay on the waiting list.", Toast.LENGTH_SHORT).show(); + private void handleStay(Context context, String userID, String eventID) { + // No action needed as the user remains in 'waitlisted' + Toast.makeText(context, "You remain on the waiting list.", Toast.LENGTH_SHORT).show(); + Log.d("NotificationActionReceiver", "User chose to stay on the waiting list."); } /** - * Handles the "Leave" action for a waiting list. Updates the invitation status in Firestore to "left_waitlist". + * Handles the "Leave" action. Moves the entrant from 'waitlisted' to 'cancelled'. * - * @param context The {@link Context} in which the receiver is running. - * @param invitationId The ID of the invitation to be updated. + * @param context The {@link Context} in which the receiver is running. + * @param userID The ID of the user. + * @param eventID The ID of the event. */ - private void handleLeave(Context context, String invitationId) { - db.collection("invitations").document(invitationId) - .update("status", "left_waitlist") - .addOnSuccessListener(aVoid -> { - Toast.makeText(context, "You have left the waiting list", Toast.LENGTH_SHORT).show(); + private void handleLeave(Context context, String userID, String eventID) { + // Query to find the user's document in 'waitlisted' collection + db.collection("waitlisted") + .whereEqualTo("userID", userID) + .whereEqualTo("eventID", eventID) + .get() + .addOnSuccessListener(querySnapshot -> { + if (!querySnapshot.isEmpty()) { + DocumentSnapshot waitlistedDoc = querySnapshot.getDocuments().get(0); + DocumentReference waitlistedRef = waitlistedDoc.getReference(); + DocumentReference cancelledRef = db.collection("cancelled").document(); + + WriteBatch batch = db.batch(); + batch.set(cancelledRef, waitlistedDoc.getData()); + batch.delete(waitlistedRef); + + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(context, "You have left the waiting list.", Toast.LENGTH_SHORT).show(); + Log.d("NotificationActionReceiver", "User moved to 'cancelled'."); + }) + .addOnFailureListener(e -> { + Toast.makeText(context, "Error leaving the waiting list.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Transaction failed: " + e.getMessage()); + }); + } else { + Toast.makeText(context, "Waitlisted document not found.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Waitlisted document not found for userID: " + userID + ", eventID: " + eventID); + } }) .addOnFailureListener(e -> { - Toast.makeText(context, "Error leaving the waiting list", Toast.LENGTH_SHORT).show(); + Toast.makeText(context, "Error fetching waitlisted document.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionReceiver", "Error fetching waitlisted document: ", e); }); } } diff --git a/app/src/main/java/com/example/vortex_app/model/NotificationModel.java b/app/src/main/java/com/example/vortex_app/model/NotificationModel.java index 2353d1f..7ac7da7 100644 --- a/app/src/main/java/com/example/vortex_app/model/NotificationModel.java +++ b/app/src/main/java/com/example/vortex_app/model/NotificationModel.java @@ -17,6 +17,8 @@ public class NotificationModel { private String status; private String id; // Firestore document ID private Date TimeStamp; + private String eventID; + private String userID; /** * Constructs a {@code NotificationModel} instance with the specified parameters. @@ -27,12 +29,14 @@ public class NotificationModel { * @param id The Firestore document ID associated with the notification. * @param Timestamp The timestamp indicating when the notification was created or last updated. */ - public NotificationModel(String title, String message, String status, String id, Date Timestamp) { + public NotificationModel(String title, String message, String status, String id, Date Timestamp, String eventID, String userID) { this.title = title; this.message = message; this.status = status; this.id = id; this.TimeStamp = Timestamp; + this.eventID = eventID; + this.userID = userID; } /** @@ -79,4 +83,12 @@ public String getId() { public Date getTimeStamp() { return TimeStamp; } + + public String getEventID() {return eventID;} + + + public String getUserID() {return userID;} } + + + diff --git a/app/src/main/java/com/example/vortex_app/model/User.java b/app/src/main/java/com/example/vortex_app/model/User.java index a656449..5113cfa 100644 --- a/app/src/main/java/com/example/vortex_app/model/User.java +++ b/app/src/main/java/com/example/vortex_app/model/User.java @@ -13,6 +13,7 @@ public class User { private String userID; // Unique ID of the user private String device; private String eventID; + private Boolean notificationsEnabled; @@ -26,12 +27,14 @@ public User(String firstName, String lastName, String email, String contactInfo, this.email = email; this.contactInfo = contactInfo; this.device = device; + //this.notificationsEnabled = notificationsEnabled; } public User(String firstName, String lastName, String userID) { this.firstName = firstName; this.lastName = lastName; this.userID = userID; + //this.notificationsEnabled = true; } @@ -103,6 +106,15 @@ public String getFullName() { return firstName + " " + lastName; } + + public Boolean getNotificationsEnabled() { + return notificationsEnabled; + } + + public void setNotificationsEnabled(Boolean notificationsEnabled) { + this.notificationsEnabled = notificationsEnabled; + } + @Override public String toString() { return "User{" + @@ -113,6 +125,10 @@ public String toString() { ", avatarUrl='" + avatarUrl + '\'' + ", userID='" + userID + '\'' + ", device='" + device + '\'' + + ", notificationsEnabled=" + notificationsEnabled + '}'; } -} \ No newline at end of file +} + + + diff --git a/app/src/main/java/com/example/vortex_app/view/notification/NotificationsActivity.java b/app/src/main/java/com/example/vortex_app/view/notification/NotificationsActivity.java index 52233de..1a0b9bd 100644 --- a/app/src/main/java/com/example/vortex_app/view/notification/NotificationsActivity.java +++ b/app/src/main/java/com/example/vortex_app/view/notification/NotificationsActivity.java @@ -1,27 +1,49 @@ package com.example.vortex_app.view.notification; - +import android.Manifest; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; import android.util.Log; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; + import com.example.vortex_app.R; +import com.example.vortex_app.controller.NotificationActionReceiver; import com.example.vortex_app.model.NotificationModel; import com.example.vortex_app.controller.adapter.NotificationAdapter; import com.example.vortex_app.view.entrant.EntrantActivity; import com.example.vortex_app.view.event.EventActivity; import com.example.vortex_app.view.profile.ProfileActivity; import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseUser; +import com.google.firebase.firestore.DocumentChange; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.Query; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * {@code NotificationsActivity} displays a list of notifications for the user. @@ -37,6 +59,34 @@ public class NotificationsActivity extends AppCompatActivity { private NotificationAdapter adapter; private List notificationList = new ArrayList<>(); private FirebaseFirestore db; + private static final int REQUEST_CODE_POST_NOTIFICATIONS = 1001; + private ListenerRegistration notificationListener; + + + private FirebaseAuth mAuth; + + + // Define action strings + public static final String ACTION_ACCEPT = "ACTION_ACCEPT"; + public static final String ACTION_DECLINE = "ACTION_DECLINE"; + public static final String ACTION_STAY = "ACTION_STAY"; + public static final String ACTION_LEAVE = "ACTION_LEAVE"; + + + private SwitchCompat switchNotifications; + + + + @Override protected void onStart() { + super.onStart(); + + } + @Override protected void onStop(){ + super.onStop(); + if (notificationListener != null) { + notificationListener.remove(); + } + } /** * Called when the activity is first created. @@ -52,6 +102,15 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_notifications); + // Initialize Firestore + db = FirebaseFirestore.getInstance(); + + // Initialize RecyclerView + recyclerView = findViewById(R.id.recyclerView); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + adapter = new NotificationAdapter(notificationList, this); + recyclerView.setAdapter(adapter); + // Set up bottom navigation BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation); bottomNavigationView.setSelectedItemId(R.id.nav_events); @@ -73,17 +132,395 @@ protected void onCreate(Bundle savedInstanceState) { return false; }); - // Initialize RecyclerView - recyclerView = findViewById(R.id.recyclerView); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - adapter = new NotificationAdapter(notificationList, this); - recyclerView.setAdapter(adapter); - // Initialize Firestore - db = FirebaseFirestore.getInstance(); + + + // Check and request notification permission + checkAndRequestNotificationPermission(); + + // Initialize the switch state based on Firestore data + //initializeNotificationPreference(); + + // Set a listener to handle toggle changes + + + + + // Fetch existing notifications fetchNotifications(); + + // Listen for new notifications in Firestore + listenForNotifications(); + } + + + /** + * Updates the user's notification preference in Firestore. + * + * @param isEnabled Whether the user has enabled notifications. + */ + private void updateNotificationPreference(boolean isEnabled) { + FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + if (currentUser == null) { + // Handle unauthenticated user + Log.e("NotificationsActivity", "User not authenticated."); + return; + } + + String currentUserID = currentUser.getUid(); + + db.collection("user_profile") + .document(currentUserID) + .update("notificationsEnabled", isEnabled) + .addOnSuccessListener(aVoid -> { + if (isEnabled) { + Toast.makeText(this, "Notifications enabled.", Toast.LENGTH_SHORT).show(); + // Re-initialize the listener + listenForNotifications(); + } else { + Toast.makeText(this, "Notifications disabled.", Toast.LENGTH_SHORT).show(); + // Remove existing notifications from device + clearLocalNotifications(); + // Remove listener + if (notificationListener != null) { + notificationListener.remove(); + notificationListener = null; + } + // Optionally, update Firestore to mark notifications as read or delete them + //clearFirestoreNotifications(currentUserID); + } + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Failed to update notification preference.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationsActivity", "Error updating notification preference.", e); + }); + } + + + /** + * Clears all local notifications from the device. + */ + + private void clearLocalNotifications() { + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null) { + notificationManager.cancelAll(); + } + } + + + /** + * Initializes the notification preference switch based on Firestore data. + */ + private void initializeNotificationPreference() { + FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + if (currentUser == null) { + // Handle unauthenticated user + Log.e("NotificationsActivity", "User not authenticated."); + return; + } + + String currentUserID = currentUser.getUid(); + + db.collection("user_profile") + .document(currentUserID) + .get() + .addOnSuccessListener(documentSnapshot -> { + if (documentSnapshot.exists()) { + Boolean notificationsEnabled = documentSnapshot.getBoolean("notificationsEnabled"); + if (notificationsEnabled != null) { + switchNotifications.setChecked(notificationsEnabled); + } else { + // Field doesn't exist, set default to true + switchNotifications.setChecked(true); + //Set default notification preference + Map defaultPref = new HashMap<>(); + defaultPref.put("notificationsEnabled", true); + db.collection("user_profile") + .document(currentUserID) + .update(defaultPref) + .addOnSuccessListener(aVoid -> Log.d("NotificationsActivity", "Default notification preference set.")) + .addOnFailureListener(e -> Log.e("NotificationsActivity", "Failed to set default notification preference.", e)); + } + } else { + // User document doesn't exist, create it with default notification preference + + + Map userData = new HashMap<>(); + userData.put("notificationsEnabled", true); + // Add other necessary fields as per your User class, e.g., firstName, lastName, etc. + // For example: + userData.put("firstName", currentUser.getDisplayName() != null ? currentUser.getDisplayName() : "FirstName"); + userData.put("lastName", "LastName"); // Replace with actual data if available + userData.put("email", currentUser.getEmail()); + + db.collection("user_profile") + .document(currentUserID) + .set(userData) + .addOnSuccessListener(aVoid -> { + switchNotifications.setChecked(true); + Log.d("NotificationsActivity", "User profile created with default notification preference."); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Error creating user profile.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationsActivity", "Error creating user profile.", e); + }); + } + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Error fetching user data.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationsActivity", "Error fetching user data.", e); + }); + + } + + + + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode == REQUEST_CODE_POST_NOTIFICATIONS) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Permission granted + Toast.makeText(this, "Notification permission granted.", Toast.LENGTH_SHORT).show(); + } else { + // Permission denied + Toast.makeText(this, "Notification permission denied. Notifications will not be displayed.", Toast.LENGTH_SHORT).show(); + // You may still proceed, but notifications won't be displayed + } + } + } + + /** + * Checks and requests notification permission if necessary (for Android 13+). + */ + private void checkAndRequestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13 or higher + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) { + // Show an explanation to the user + new AlertDialog.Builder(this) + .setTitle("Notification Permission Needed") + .setMessage("This app requires notification permission to inform you about important updates.") + .setPositiveButton("OK", (dialog, which) -> { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_CODE_POST_NOTIFICATIONS); + }) + .setNegativeButton("Cancel", null) + .create() + .show(); + } else { + // No explanation needed; request the permission + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + REQUEST_CODE_POST_NOTIFICATIONS); + } + } else { + // Permission already granted + // No action needed + } + } else { + // Permission is automatically granted on SDK versions below 33 + // No action needed + } + } + + + + + + + /** + * Listens for new notifications in the 'notifications' collection and sends local notifications accordingly, + * only if the user has enabled notifications. + */ + private void listenForNotifications() { + FirebaseUser currentUser = FirebaseAuth.getInstance().getCurrentUser(); + if (currentUser == null) { + Log.e("NotificationsActivity", "User not authenticated."); + return; + } + String currentUserId = currentUser.getUid(); + + // Set up a listener on the 'notifications' collection for the current user + notificationListener = db.collection("notifications") + .whereEqualTo("userID", currentUserId) + .orderBy("timestamp", Query.Direction.DESCENDING) + .addSnapshotListener((querySnapshot, e) -> { + if (e != null) { + // Handle any errors + Log.w("NotificationsActivity", "Listen failed.", e); + return; + } + + if (querySnapshot != null) { + // Loop through the document changes + for (DocumentChange dc : querySnapshot.getDocumentChanges()) { + switch (dc.getType()) { + case ADDED: + // Deserialize the document into a NotificationModel object + NotificationModel notification = dc.getDocument().toObject(NotificationModel.class); + + // Add the notification to the list and update the adapter + notificationList.add(0, notification); // Add to the top of the list + adapter.notifyItemInserted(0); + + // Determine the notification type based on title or other fields + String notificationType = "lost"; // Default + if (notification.getTitle().contains("Congratulations")) { + notificationType = "selected"; + } + + // Send a local notification using the title and message from the notification + sendNotification(notification.getTitle(), notification.getMessage(), + notification.getUserID(), notification.getEventID(), notificationType); + break; + + case MODIFIED: + // Handle modified notifications if necessary + break; + + case REMOVED: + // Handle removed notifications if necessary + break; + } + } + } + }); } + + + /** + * Sends a local notification with action buttons based on the notification type. + * + * @param title The title of the notification. + * @param message The message/content of the notification. + * @param userID The ID of the user receiving the notification. + * @param eventID The ID of the associated event. + * @param notificationType The type of notification ("selected" for winners, "lost" for non-winners). + */ + private void sendNotification(String title, String message, String userID, String eventID, String notificationType) { + createNotificationChannel(); + + Intent intent = new Intent(this, NotificationDetailActivity.class); + intent.putExtra("title", title); + intent.putExtra("message", message); + intent.putExtra("userID", userID); // Pass userID and eventID if needed in detail + intent.putExtra("eventID", eventID); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + PendingIntent pendingIntent = PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + // Create action intents based on notification type + PendingIntent actionIntent1 = null; + PendingIntent actionIntent2 = null; + String actionButton1 = ""; + String actionButton2 = ""; + + if ("selected".equals(notificationType)) { + // For winners: Accept and Decline + Intent acceptIntent = new Intent(this, NotificationActionReceiver.class); + acceptIntent.setAction(ACTION_ACCEPT); + acceptIntent.putExtra("userID", userID); + acceptIntent.putExtra("eventID", eventID); + + Intent declineIntent = new Intent(this, NotificationActionReceiver.class); + declineIntent.setAction(ACTION_DECLINE); + declineIntent.putExtra("userID", userID); + declineIntent.putExtra("eventID", eventID); + + actionIntent1 = PendingIntent.getBroadcast(this, 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + actionIntent2 = PendingIntent.getBroadcast(this, 1, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + actionButton1 = "Accept"; + actionButton2 = "Decline"; + } else if ("lost".equals(notificationType)) { + // For non-winners: Stay and Leave + Intent stayIntent = new Intent(this, NotificationActionReceiver.class); + stayIntent.setAction(ACTION_STAY); + stayIntent.putExtra("userID", userID); + stayIntent.putExtra("eventID", eventID); + + Intent leaveIntent = new Intent(this, NotificationActionReceiver.class); + leaveIntent.setAction(ACTION_LEAVE); + leaveIntent.putExtra("userID", userID); + leaveIntent.putExtra("eventID", eventID); + + actionIntent1 = PendingIntent.getBroadcast(this, 2, stayIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + actionIntent2 = PendingIntent.getBroadcast(this, 3, leaveIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + actionButton1 = "Stay"; + actionButton2 = "Leave"; + } + + // Build the notification + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "status_channel_id") + .setSmallIcon(R.drawable.notification) // Use your app's notification icon + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) // Use high priority for action buttons + .setContentIntent(pendingIntent) + .setAutoCancel(true); + + // Add action buttons if applicable + if (actionIntent1 != null && actionIntent2 != null) { + builder.addAction(R.drawable.ic_notifications, actionButton1, actionIntent1); + builder.addAction(R.drawable.ic_notifications, actionButton2, actionIntent2); + } + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + + int notificationId = (int) System.currentTimeMillis(); + + // Check notification permission for Android 13+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + == PackageManager.PERMISSION_GRANTED) { + notificationManager.notify(notificationId, builder.build()); + } else { + Log.w("NotificationsActivity", "POST_NOTIFICATIONS permission not granted, cannot display notification."); + } + } else { + notificationManager.notify(notificationId, builder.build()); + } + } + + + /** + * Creates a notification channel for Android Oreo and above. + */ + private void createNotificationChannel(){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = "Status Updates"; + String description = "Notifications for status changes in events"; + int importance = NotificationManager.IMPORTANCE_HIGH; // Use high importance for action buttons + NotificationChannel channel = new NotificationChannel("status_channel_id", name, importance); + channel.setDescription(description); + + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + }} + + + + + + + + + /** * Fetches notifications from Firestore and updates the {@link RecyclerView} with the retrieved data. * This method orders notifications by timestamp in descending order and refreshes the UI upon data retrieval. @@ -101,8 +538,10 @@ private void fetchNotifications() { String status = document.getString("status"); Date Date = document.getDate("timestamp"); String id = document.getId(); + String userID = document.getString("userID"); + String eventID = document.getString("eventID"); - NotificationModel notification = new NotificationModel(title, message, status, id, Date); + NotificationModel notification = new NotificationModel(title, message, status, id, Date, eventID, userID); notificationList.add(notification); } adapter.notifyDataSetChanged(); diff --git a/app/src/main/java/com/example/vortex_app/view/profile/ProfileActivity.java b/app/src/main/java/com/example/vortex_app/view/profile/ProfileActivity.java index 5af3d47..a5ddab9 100644 --- a/app/src/main/java/com/example/vortex_app/view/profile/ProfileActivity.java +++ b/app/src/main/java/com/example/vortex_app/view/profile/ProfileActivity.java @@ -160,6 +160,7 @@ private void initializeUserData(String androidId) { "default@example.com", // Default email "123-456-7890", // Default contact information deviceInfo // Device information + //true ); // Save the default user data to Firestore diff --git a/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java b/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java index 4767a83..23ccbc9 100644 --- a/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java +++ b/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java @@ -1,6 +1,7 @@ package com.example.vortex_app.view.waitinglist; import android.os.Bundle; +import android.util.Log; import android.widget.Button; import android.widget.Toast; @@ -12,13 +13,18 @@ import com.example.vortex_app.R; import com.example.vortex_app.model.User; import com.example.vortex_app.model.orgList; +import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.WriteBatch; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class OrgWaitingListActivity extends AppCompatActivity { @@ -73,10 +79,19 @@ private void fetchWaitingList(String eventID) { } } else { Toast.makeText(this, "Failed to fetch waiting list", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Error fetching waitlisted users: ", task.getException()); } }); } + + + + /** + * Selects users from the waiting list based on the event's maxPeople limit, + * moves them to the 'selected' collection, removes them from the 'waitlisted' collection, + * and logs notifications for each selected and non-selected user based on their preference. + */ private void selectAndStoreUsers() { // Fetch maxPeople for the event db.collection("events") @@ -85,33 +100,124 @@ private void selectAndStoreUsers() { .addOnSuccessListener(documentSnapshot -> { if (documentSnapshot.exists()) { String maxPeopleStr = documentSnapshot.getString("maxPeople"); - int maxPeople = maxPeopleStr != null ? Integer.parseInt(maxPeopleStr) : 0; - + int maxPeople = 0; + try { + maxPeople = maxPeopleStr != null ? Integer.parseInt(maxPeopleStr) : 0; + } catch (NumberFormatException e) { + Toast.makeText(this, "Invalid maxPeople value.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Invalid maxPeople value: ", e); + return; + } if (maxPeople > 0 && waitingListEntrants.size() >= maxPeople) { + // Shuffle the list to randomize selection List shuffledList = new ArrayList<>(waitingListEntrants); Collections.shuffle(shuffledList); List selectedUsers = shuffledList.subList(0, maxPeople); + List nonSelectedUsers = shuffledList.subList(maxPeople, shuffledList.size()); + // Initialize Firestore WriteBatch + WriteBatch batch = db.batch(); + // Process selected users for (User user : selectedUsers) { - orgList selectedUser = new orgList(user.getUserID(), eventID); - db.collection("selected") - .add(selectedUser) - .addOnSuccessListener(documentReference -> { + String userID = user.getUserID(); + + // Query to find the specific document in 'waitlisted' for this user and event + db.collection("waitlisted") + .whereEqualTo("userID", userID) + .whereEqualTo("eventID", eventID) + .get() + .addOnSuccessListener(querySnapshot -> { + if (!querySnapshot.isEmpty()) { + DocumentSnapshot waitlistedDoc = querySnapshot.getDocuments().get(0); + DocumentReference waitlistedRef = waitlistedDoc.getReference(); + + // Reference to add to 'selected' + DocumentReference selectedRef = db.collection("selected").document(); + orgList selectedUserObj = new orgList(userID, eventID); + batch.set(selectedRef, selectedUserObj); + + // Reference to delete from 'waitlisted' + batch.delete(waitlistedRef); + + // Reference to log notification + Map selectedNotificationData = new HashMap<>(); + selectedNotificationData.put("eventID", eventID); + selectedNotificationData.put("timestamp", new Date()); + selectedNotificationData.put("title", "Congratulations!"); + selectedNotificationData.put("message", "You've been selected for the event."); + selectedNotificationData.put("userID", userID); + selectedNotificationData.put("status", "unread"); + + DocumentReference selectedNotificationRef = db.collection("notifications").document(); + batch.set(selectedNotificationRef, selectedNotificationData); + + // Commit the batch after all selected users are processed + // (This simplistic approach works if the number is small) + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(this, "Draw completed successfully.", Toast.LENGTH_SHORT).show(); + // Refresh the waiting list + fetchWaitingList(eventID); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Failed to perform the draw.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Error committing batch: ", e); + }); + } else { + Toast.makeText(this, "Waitlisted document not found for user: " + userID, Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Waitlisted document not found for userID: " + userID); + } }) .addOnFailureListener(e -> { - Toast.makeText(this, "Failed to store selected user", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, "Error fetching waitlisted document.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Error fetching waitlisted document: ", e); }); } - Toast.makeText(this, "Selected users stored successfully", Toast.LENGTH_SHORT).show(); + + // Process non-selected users + for (User user : nonSelectedUsers) { + String userID = user.getUserID(); + + // Log "Better Luck Next Time" notification + Map lostNotificationData = new HashMap<>(); + lostNotificationData.put("eventID", eventID); + lostNotificationData.put("timestamp", new Date()); + lostNotificationData.put("title", "Better Luck Next Time"); + lostNotificationData.put("message", "You have not been selected for the event."); + lostNotificationData.put("userID", userID); + lostNotificationData.put("status", "unread"); + + DocumentReference lostNotificationRef = db.collection("notifications").document(); + batch.set(lostNotificationRef, lostNotificationData); + } + + // Commit the batch for non-selected users + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(this, "Draw completed successfully.", Toast.LENGTH_SHORT).show(); + // Refresh the waiting list + fetchWaitingList(eventID); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Failed to perform the draw.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Error committing batch: ", e); + }); + } else { - Toast.makeText(this, "Not enough users in the waiting list or invalid maxPeople", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, "Not enough users in the waiting list or invalid maxPeople.", Toast.LENGTH_SHORT).show(); } } else { - Toast.makeText(this, "Event details not found", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, "Event details not found.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Event document does not exist."); } }) - .addOnFailureListener(e -> Toast.makeText(this, "Failed to fetch event details", Toast.LENGTH_SHORT).show()); + .addOnFailureListener(e -> { + Toast.makeText(this, "Failed to fetch event details.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Error fetching event details: ", e); + }); } + + } diff --git a/app/src/main/res/layout/activity_notifications.xml b/app/src/main/res/layout/activity_notifications.xml index 6155703..80db4c8 100644 --- a/app/src/main/res/layout/activity_notifications.xml +++ b/app/src/main/res/layout/activity_notifications.xml @@ -1,5 +1,6 @@ - + + + + + + + + + + - + + From 4325151d21ae465dfaa206351845c454baff9aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSagargit=20config=20--global=20user=2Eemail=20?= =?UTF-8?q?=E2=80=9Cmsagar2606=40gmail=2Ecom?= Date: Sun, 1 Dec 2024 13:25:40 -0700 Subject: [PATCH 2/6] Update OrgWaitingListActivity.java --- .../vortex_app/view/waitinglist/OrgWaitingListActivity.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java b/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java index 23ccbc9..dd7a7c1 100644 --- a/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java +++ b/app/src/main/java/com/example/vortex_app/view/waitinglist/OrgWaitingListActivity.java @@ -70,7 +70,8 @@ private void fetchWaitingList(String eventID) { for (DocumentSnapshot document : querySnapshot) { String firstName = document.getString("firstName"); String lastName = document.getString("lastName"); - String userID = document.getString("userID"); // Assuming userID is stored + String userID = document.getString("userID"); + //String eventID = document.getString("eventID");// Assuming userID is stored User user = new User(firstName, lastName, userID); waitingListEntrants.add(user); @@ -134,7 +135,7 @@ private void selectAndStoreUsers() { DocumentReference waitlistedRef = waitlistedDoc.getReference(); // Reference to add to 'selected' - DocumentReference selectedRef = db.collection("selected").document(); + DocumentReference selectedRef = db.collection("selected_but_not_confirmed").document(); orgList selectedUserObj = new orgList(userID, eventID); batch.set(selectedRef, selectedUserObj); From def63237b196a6add721ae1c189f6f2f3d7c4d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSagargit=20config=20--global=20user=2Eemail=20?= =?UTF-8?q?=E2=80=9Cmsagar2606=40gmail=2Ecom?= Date: Sun, 1 Dec 2024 13:26:24 -0700 Subject: [PATCH 3/6] Update NotificationActionReceiver.java --- .../vortex_app/controller/NotificationActionReceiver.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java b/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java index 3e0ce44..ef65eaa 100644 --- a/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java +++ b/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java @@ -70,7 +70,7 @@ public void onReceive(Context context, Intent intent) { */ private void handleAccept(Context context, String userID, String eventID) { // Query to find the user's document in 'selected' collection - db.collection("selected") + db.collection("selected_but_not_confirmed") .whereEqualTo("userID", userID) .whereEqualTo("eventID", eventID) .get() @@ -113,7 +113,7 @@ private void handleAccept(Context context, String userID, String eventID) { */ private void handleDecline(Context context, String userID, String eventID) { // Query to find the user's document in 'selected' collection - db.collection("selected") + db.collection("selected_but_not_confirmed") .whereEqualTo("userID", userID) .whereEqualTo("eventID", eventID) .get() From ddc53dbf067dc7143b7282adcc2ae4892d9905a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSagargit=20config=20--global=20user=2Eemail=20?= =?UTF-8?q?=E2=80=9Cmsagar2606=40gmail=2Ecom?= Date: Sun, 1 Dec 2024 13:26:55 -0700 Subject: [PATCH 4/6] merge commit --- .../vortex_app/controller/NotificationActionReceiver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java b/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java index ef65eaa..05dcf4b 100644 --- a/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java +++ b/app/src/main/java/com/example/vortex_app/controller/NotificationActionReceiver.java @@ -182,7 +182,7 @@ private void handleLeave(Context context, String userID, String eventID) { WriteBatch batch = db.batch(); batch.set(cancelledRef, waitlistedDoc.getData()); batch.delete(waitlistedRef); - + // this one batch.commit() .addOnSuccessListener(aVoid -> { Toast.makeText(context, "You have left the waiting list.", Toast.LENGTH_SHORT).show(); From 26b054d0b676ec064cbf7068ea07e044e38975ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSagargit=20config=20--global=20user=2Eemail=20?= =?UTF-8?q?=E2=80=9Cmsagar2606=40gmail=2Ecom?= Date: Sun, 1 Dec 2024 22:37:17 -0700 Subject: [PATCH 5/6] Update CancelledEntrantAdapter.java --- .../vortex_app/controller/adapter/CancelledEntrantAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/example/vortex_app/controller/adapter/CancelledEntrantAdapter.java b/app/src/main/java/com/example/vortex_app/controller/adapter/CancelledEntrantAdapter.java index 88732f9..4b8b880 100644 --- a/app/src/main/java/com/example/vortex_app/controller/adapter/CancelledEntrantAdapter.java +++ b/app/src/main/java/com/example/vortex_app/controller/adapter/CancelledEntrantAdapter.java @@ -15,7 +15,7 @@ public class CancelledEntrantAdapter extends RecyclerView.Adapter { - private List entrantList; + private final List entrantList; public CancelledEntrantAdapter(List entrantList) { this.entrantList = entrantList; From 94b4611c110a7351aadd474861cd98d3c23f6b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CSagargit=20config=20--global=20user=2Eemail=20?= =?UTF-8?q?=E2=80=9Cmsagar2606=40gmail=2Ecom?= Date: Sun, 1 Dec 2024 22:41:25 -0700 Subject: [PATCH 6/6] Update AndroidManifest.xml --- app/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 907674d..109d268 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -59,6 +59,8 @@ + +