diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index 6b4eeac..b268ef3 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -5,9 +5,6 @@ - - \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c2e3cbe..109d268 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,8 +6,7 @@ - - + @@ -36,7 +35,7 @@ + android:value="YOUR_GOOGLE_MAPS_API_KEY" /> - - - + + android:exported="true"> + + + + + + + + + + + + + - 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 a883140..03b71ea 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 @@ -3,111 +3,80 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.util.Log; import android.widget.Toast; -import com.google.firebase.firestore.FirebaseFirestore; +import com.example.vortex_app.controller.adapter.NotificationActionHandler; -/** - * {@code NotificationActionReceiver} is a {@link BroadcastReceiver} that handles actions - * from notification buttons, such as accepting or declining an invitation, staying on or leaving a waiting list. - * It updates the invitation status in Firestore based on the user's response. - * - *

This receiver allows the app to perform actions in response to user interactions with notifications - * without needing to open an activity. - */ public class NotificationActionReceiver extends BroadcastReceiver { - private FirebaseFirestore db; + // Define unique action strings + public static final String ACTION_ACCEPT = "com.example.vortex_app.ACTION_ACCEPT"; + public static final String ACTION_DECLINE = "com.example.vortex_app.ACTION_DECLINE"; + public static final String ACTION_STAY = "com.example.vortex_app.ACTION_STAY"; + public static final String ACTION_LEAVE = "com.example.vortex_app.ACTION_LEAVE"; + - /** - * 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. - */ @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"); + + Log.d("NotificationActionReceiver", "Received action: " + action + ", userID: " + userID + ", eventID: " + eventID); + + if (userID == null || eventID == null) { + Log.e("NotificationActionReceiver", "Missing userID or eventID."); + return; + } + + NotificationActionHandler handler = new NotificationActionHandler(); switch (action) { - case "ACTION_ACCEPT": - handleAccept(context, invitationId); + case ACTION_ACCEPT: + Log.d("NotificationActionReceiver", "Handling ACTION_ACCEPT"); + handler.moveDocument( + "selected_but_not_confirmed", + "final", + userID, + eventID, + context, + "You have been enrolled!", + "Error enrolling in the event." + ); break; - case "ACTION_DECLINE": - handleDecline(context, invitationId); + case ACTION_DECLINE: + Log.d("NotificationActionReceiver", "Handling ACTION_DECLINE"); + handler.moveDocument( + "selected_but_not_confirmed", + "waitlisted", + userID, + eventID, + context, + "You have declined the invitation.", + "Error declining the invitation." + ); break; - case "ACTION_STAY": - handleStay(context); + case ACTION_STAY: + Log.d("NotificationActionReceiver", "Handling ACTION_STAY"); + Toast.makeText(context, "You remain on the waiting list.", Toast.LENGTH_SHORT).show(); + Log.d("NotificationActionReceiver", "User chose to stay on the waiting list."); break; - case "ACTION_LEAVE": - handleLeave(context, invitationId); + case ACTION_LEAVE: + Log.d("NotificationActionReceiver", "Handling ACTION_LEAVE"); + handler.moveDocument( + "waitlisted", + "cancelled", + userID, + eventID, + context, + "You have left the waiting list.", + "Error leaving the waiting list." + ); 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". - * - * @param context The {@link Context} in which the receiver is running. - * @param invitationId The ID of the invitation to be updated. - */ - 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(); - }) - .addOnFailureListener(e -> { - Toast.makeText(context, "Error accepting invitation", Toast.LENGTH_SHORT).show(); - }); - } - - /** - * Handles the "Decline" action for an invitation. Updates the invitation status in Firestore to "declined". - * - * @param context The {@link Context} in which the receiver is running. - * @param invitationId The ID of the invitation to be updated. - */ - 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(); - }) - .addOnFailureListener(e -> { - Toast.makeText(context, "Error declining invitation", Toast.LENGTH_SHORT).show(); - }); - } - - /** - * 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. - * - * @param context The {@link Context} in which the receiver is running. - */ - private void handleStay(Context context) { - Toast.makeText(context, "You have chosen to stay on the waiting list.", Toast.LENGTH_SHORT).show(); - } - - /** - * Handles the "Leave" action for a waiting list. Updates the invitation status in Firestore to "left_waitlist". - * - * @param context The {@link Context} in which the receiver is running. - * @param invitationId The ID of the invitation to be updated. - */ - 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(); - }) - .addOnFailureListener(e -> { - Toast.makeText(context, "Error leaving the waiting list", Toast.LENGTH_SHORT).show(); - }); - } } diff --git a/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationActionHandler.java b/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationActionHandler.java new file mode 100644 index 0000000..e6c1399 --- /dev/null +++ b/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationActionHandler.java @@ -0,0 +1,66 @@ +package com.example.vortex_app.controller.adapter; + + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.WriteBatch; + +public class NotificationActionHandler { + + private FirebaseFirestore db; + + public NotificationActionHandler() { + db = FirebaseFirestore.getInstance(); + } + + /** + * Moves a document from sourceCollection to targetCollection. + * + * @param sourceCollection The source Firestore collection. + * @param targetCollection The target Firestore collection. + * @param userID The user ID. + * @param eventID The event ID. + * @param context The context to show Toast messages. + * @param successMessage The message to show on success. + * @param failureMessage The message to show on failure. + */ + public void moveDocument(String sourceCollection, String targetCollection, String userID, String eventID, Context context, String successMessage, String failureMessage) { + db.collection(sourceCollection) + .whereEqualTo("userID", userID) + .whereEqualTo("eventID", eventID) + .get() + .addOnSuccessListener(querySnapshot -> { + if (!querySnapshot.isEmpty()) { + DocumentSnapshot doc = querySnapshot.getDocuments().get(0); + DocumentReference sourceRef = doc.getReference(); + DocumentReference targetRef = db.collection(targetCollection).document(); + + WriteBatch batch = db.batch(); + batch.set(targetRef, doc.getData()); + batch.delete(sourceRef); + + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(context, successMessage, Toast.LENGTH_SHORT).show(); + Log.d("NotificationActionHandler", "User moved to '" + targetCollection + "'."); + }) + .addOnFailureListener(e -> { + Toast.makeText(context, failureMessage, Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionHandler", "Transaction failed: " + e.getMessage()); + }); + } else { + Toast.makeText(context, "Document not found.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionHandler", "Document not found for userID: " + userID + ", eventID: " + eventID); + } + }) + .addOnFailureListener(e -> { + Toast.makeText(context, "Error fetching document.", Toast.LENGTH_SHORT).show(); + Log.e("NotificationActionHandler", "Error fetching document: ", e); + }); + } +} diff --git a/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationAdapter.java b/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationAdapter.java index ca867b6..8b31bdc 100644 --- a/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationAdapter.java +++ b/app/src/main/java/com/example/vortex_app/controller/adapter/NotificationAdapter.java @@ -1,3 +1,4 @@ + package com.example.vortex_app.controller.adapter; import android.content.Context; 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..ec73f09 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,11 @@ public class NotificationModel { private String status; private String id; // Firestore document ID private Date TimeStamp; + private String eventID; + private String userID; + private String Type; + + /** * Constructs a {@code NotificationModel} instance with the specified parameters. @@ -27,6 +32,17 @@ 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, String eventID, String userID, String Type) { + this.title = title; + this.message = message; + this.status = status; + this.id = id; + this.TimeStamp = Timestamp; + this.eventID = eventID; + this.userID = userID; + this.Type = Type; + } + public NotificationModel(String title, String message, String status, String id, Date Timestamp) { this.title = title; this.message = message; @@ -35,6 +51,7 @@ public NotificationModel(String title, String message, String status, String id, this.TimeStamp = Timestamp; } + /** * Returns the title of the notification. * @@ -71,6 +88,9 @@ public String getId() { return id; } + public void setId(String id) { + this.id = id; + } /** * Returns the timestamp of the notification. * @@ -79,4 +99,17 @@ public String getId() { public Date getTimeStamp() { return TimeStamp; } + + public String getEventID() {return eventID;} + + + public String getUserID() {return userID;} + + public String getType() {return Type;} + + public void SetType(String type) {this.Type = Type;} + } + + + diff --git a/app/src/main/java/com/example/vortex_app/view/notification/NotificationClickListener.java b/app/src/main/java/com/example/vortex_app/view/notification/NotificationClickListener.java new file mode 100644 index 0000000..cbf6844 --- /dev/null +++ b/app/src/main/java/com/example/vortex_app/view/notification/NotificationClickListener.java @@ -0,0 +1,9 @@ +package com.example.vortex_app.view.notification; + + + +import com.example.vortex_app.model.NotificationModel; + +public interface NotificationClickListener { + void onNotificationClick(NotificationModel notification); +} diff --git a/app/src/main/java/com/example/vortex_app/view/notification/NotificationDetailActivity.java b/app/src/main/java/com/example/vortex_app/view/notification/NotificationDetailActivity.java index cb1d47b..0663aad 100644 --- a/app/src/main/java/com/example/vortex_app/view/notification/NotificationDetailActivity.java +++ b/app/src/main/java/com/example/vortex_app/view/notification/NotificationDetailActivity.java @@ -1,29 +1,44 @@ package com.example.vortex_app.view.notification; +import android.content.Intent; import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.example.vortex_app.R; +import com.example.vortex_app.controller.NotificationActionReceiver; /** * {@code NotificationDetailActivity} displays detailed information about a specific notification. - * It retrieves the notification's title, message, and other relevant data passed through an intent - * and displays it within the activity's layout. + * It retrieves the notification's title, message, date, and other relevant data passed through an intent + * and displays them within the activity's layout. * *

This activity also includes a back button in the action bar to allow users to return to the previous screen. + * Additionally, it displays action buttons (e.g., Accept, Decline, Stay, Leave) based on the notification type. */ public class NotificationDetailActivity extends AppCompatActivity { - /** - * Initializes the activity and sets up the view to display notification details. - * Retrieves the title and message data from the intent and assigns them to the appropriate TextViews. - * - * @param savedInstanceState If the activity is being re-initialized after previously being shut down, - * then this Bundle contains the most recent data supplied by - * {@link #onSaveInstanceState(Bundle)}. - */ + // Constants for logging + private static final String TAG = "NotificationDetailActivity"; + + // Notification data + private String title; + private String message; + private String userID; + private String eventID; + private String notificationType; + private String Date; + + // UI Components + private TextView titleTextView, messageTextView, dateTextView; + private Button buttonAction1, buttonAction2; + private View actionButtonsLayout; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -35,18 +50,107 @@ protected void onCreate(Bundle savedInstanceState) { actionBar.setDisplayHomeAsUpEnabled(true); } - // Retrieve data from the intent - String title = getIntent().getStringExtra("title"); - String message = getIntent().getStringExtra("message"); + // Initialize views + titleTextView = findViewById(R.id.titleTextView); + messageTextView = findViewById(R.id.messageTextView); + dateTextView = findViewById(R.id.dateTextView); + buttonAction1 = findViewById(R.id.detailButtonPrimary); + buttonAction2 = findViewById(R.id.detailButtonSecondary); + actionButtonsLayout = findViewById(R.id.actionButtonsLayout); + + // Retrieve data from the intent and assign to class variables + Intent intent = getIntent(); + if (intent != null) { + title = intent.getStringExtra("title"); + message = intent.getStringExtra("message"); + Date = intent.getStringExtra("date"); // Optional: Handle if not provided + userID = intent.getStringExtra("userID"); + eventID = intent.getStringExtra("eventID"); + notificationType = intent.getStringExtra("notificationType"); // Ensure this key matches sender + + // Debugging: Log the received data + Log.d(TAG, "Title: " + title); + Log.d(TAG, "Message: " + message); + Log.d(TAG, "UserID: " + userID); + Log.d(TAG, "EventID: " + eventID); + Log.d(TAG, "NotificationType: " + notificationType); + + // Optional: Show a Toast for debugging + Toast.makeText(this, "Type: " + notificationType, Toast.LENGTH_SHORT).show(); + } else { + Log.e(TAG, "No intent received."); + Toast.makeText(this, "No notification data received.", Toast.LENGTH_SHORT).show(); + } // Set data to TextViews - TextView titleTextView = findViewById(R.id.titleTextView); - TextView messageTextView = findViewById(R.id.messageTextView); - TextView dateTextView = findViewById(R.id.dateTextView); + titleTextView.setText(title != null ? title : "No Title"); + messageTextView.setText(message != null ? message : "No Message"); + + if (intent != null && intent.hasExtra("date")) { + dateTextView.setText(intent.getStringExtra("date")); + } else { + // Set to current date or a default value if date is not provided + dateTextView.setText(Date); + } + + // Configure action buttons based on notification type + configureActionButtons(); + } + + /** + * Configures the action buttons based on the notification type. + * Shows the buttons and sets their text and click listeners accordingly. + */ + private void configureActionButtons() { + if ("selected".equalsIgnoreCase(notificationType)) { + // Show Accept and Decline buttons + actionButtonsLayout.setVisibility(View.VISIBLE); + buttonAction1.setText("Accept"); + buttonAction2.setText("Decline"); + + buttonAction1.setOnClickListener(v -> sendActionBroadcast(NotificationActionReceiver.ACTION_ACCEPT, userID, eventID)); + buttonAction2.setOnClickListener(v -> sendActionBroadcast(NotificationActionReceiver.ACTION_DECLINE, userID, eventID)); + + Log.d(TAG, "Configured buttons for 'selected' type."); + Toast.makeText(this, "Configured buttons for 'selected' type.", Toast.LENGTH_SHORT).show(); + } else if ("lost".equalsIgnoreCase(notificationType)) { + // Show Stay and Leave buttons + actionButtonsLayout.setVisibility(View.VISIBLE); + buttonAction1.setText("Stay"); + buttonAction2.setText("Leave"); + + buttonAction1.setOnClickListener(v -> sendActionBroadcast(NotificationActionReceiver.ACTION_STAY, userID, eventID)); + buttonAction2.setOnClickListener(v -> sendActionBroadcast(NotificationActionReceiver.ACTION_LEAVE, userID, eventID)); + + Log.d(TAG, "Configured buttons for 'lost' type."); + Toast.makeText(this, "Configured buttons for 'lost' type.", Toast.LENGTH_SHORT).show(); + } else { + // Hide buttons if notification type is unknown or not provided + actionButtonsLayout.setVisibility(View.GONE); + Log.d(TAG, "Unknown notification type. Buttons hidden."); + Toast.makeText(this, "Unknown notification type. Buttons hidden.", Toast.LENGTH_SHORT).show(); + } + } + + /** + * Sends a broadcast intent to NotificationActionReceiver with the specified action. + * + * @param action The action string to be handled by the receiver. + * @param userID The ID of the user. + * @param eventID The ID of the event. + */ + private void sendActionBroadcast(String action, String userID, String eventID) { + Intent intent = new Intent(this, NotificationActionReceiver.class); + intent.setAction(action); + intent.putExtra("userID", userID); + intent.putExtra("eventID", eventID); + sendBroadcast(intent); + + // Provide user feedback + Toast.makeText(this, action.replace("ACTION_", "") + " clicked.", Toast.LENGTH_SHORT).show(); - titleTextView.setText(title); - messageTextView.setText(message); - dateTextView.setText("2025-01-01"); // Example date, replace with actual date if available + // Optionally, finish the activity or navigate elsewhere + finish(); } /** 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 47fc208..19684f1 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,28 +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.EventInfoActivity; import com.example.vortex_app.view.event.ManageEventsActivity; 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. @@ -38,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. @@ -53,46 +102,431 @@ 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 - // Set up bottom navigation and handle item selection BottomNavigationView bottomNavigationView = findViewById(R.id.bottom_navigation); - bottomNavigationView.setSelectedItemId(R.id.nav_notifications); - + bottomNavigationView.setSelectedItemId(R.id.nav_events); bottomNavigationView.setOnItemSelectedListener(item -> { int itemId = item.getItemId(); if (itemId == R.id.nav_home) { startActivity(new Intent(this, EntrantActivity.class)); finish(); return true; - } else if (itemId == R.id.nav_events) { - startActivity(new Intent(this, ManageEventsActivity.class)); - finish(); - return true; } else if (itemId == R.id.nav_profile) { - Intent intent = new Intent(this, ProfileActivity.class); - startActivity(intent); + startActivity(new Intent(this, ProfileActivity.class)); finish(); return true; - - } else if (itemId == R.id.nav_notifications) { - // Current activity; do nothing return true; + } else if (itemId == R.id.nav_events) { + startActivity(new Intent(this, ManageEventsActivity.class)); } 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 Type = "lost"; // Default + if (notification.getTitle().contains("Congratulations")) { + Type = "selected"; + } + + // Send a local notification using the title and message from the notification + sendNotification(notification.getTitle(), notification.getMessage(), + notification.getUserID(), notification.getEventID(), Type); + 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 Type The type of notification ("selected" for winners, "lost" for non-winners). + */ + private void sendNotification(String title, String message, String userID, String eventID, String Type) { + createNotificationChannel(); + + // Intent to open NotificationDetailActivity + Intent intent = new Intent(this, NotificationDetailActivity.class); + intent.putExtra("title", title); + intent.putExtra("message", message); + intent.putExtra("userID", userID); + intent.putExtra("eventID", eventID); + intent.putExtra("notificationType", Type); // Ensure key matches in Detail Activity + 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".equalsIgnoreCase(Type)) { + // For winners: Accept and Decline + Intent acceptIntent = new Intent(this, NotificationActionReceiver.class); + acceptIntent.setAction(NotificationActionReceiver.ACTION_ACCEPT); + acceptIntent.putExtra("userID", userID); + acceptIntent.putExtra("eventID", eventID); + + Intent declineIntent = new Intent(this, NotificationActionReceiver.class); + declineIntent.setAction(NotificationActionReceiver.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".equalsIgnoreCase(Type)) { + // For non-winners: Stay and Leave + Intent stayIntent = new Intent(this, NotificationActionReceiver.class); + stayIntent.setAction(NotificationActionReceiver.ACTION_STAY); + stayIntent.putExtra("userID", userID); + stayIntent.putExtra("eventID", eventID); + + Intent leaveIntent = new Intent(this, NotificationActionReceiver.class); + leaveIntent.setAction(NotificationActionReceiver.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) // Ensure this icon exists + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) // High priority for action buttons + .setContentIntent(pendingIntent) + .setAutoCancel(true); + + // Add action buttons if applicable + if (actionIntent1 != null && actionIntent2 != null) { + // Use distinct icons for actions if available + builder.addAction(R.drawable.ic_acceptt, actionButton1, actionIntent1); + builder.addAction(R.drawable.ic_decline, actionButton2, actionIntent2); + } + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); + + int notificationId = (int) System.currentTimeMillis(); // Unique ID + + // 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()); + } + + Log.d("NotificationsActivity", "Notification sent: " + title); + } + + + + /** + * 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. @@ -110,8 +544,11 @@ 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"); + String type = document.getString("notificationType"); - NotificationModel notification = new NotificationModel(title, message, status, id, Date); + NotificationModel notification = new NotificationModel(title, message, status, id, Date, eventID, userID, type); notificationList.add(notification); } adapter.notifyDataSetChanged(); @@ -137,6 +574,12 @@ private void handleNotificationClick(NotificationModel notification) { .addOnFailureListener(e -> Log.e("Firestore", "Error updating status", e)); } + /** + * Opens the detailed view of a selected notification in {@link NotificationDetailActivity}. + * Marks the notification as read in Firestore before transitioning to the detailed view. + * + * @param notification The {@link NotificationModel} object representing the notification to be opened in detail view. + */ /** * Opens the detailed view of a selected notification in {@link NotificationDetailActivity}. * Marks the notification as read in Firestore before transitioning to the detailed view. @@ -152,10 +595,13 @@ private void openNotificationDetail(NotificationModel notification) { Intent intent = new Intent(this, NotificationDetailActivity.class); intent.putExtra("title", notification.getTitle()); intent.putExtra("message", notification.getMessage()); - intent.putExtra("status", notification.getStatus()); + intent.putExtra("date", notification.getTimeStamp() != null ? notification.getTimeStamp().toString() : "Unknown Date"); + intent.putExtra("userID", notification.getUserID()); + intent.putExtra("eventID", notification.getEventID()); + intent.putExtra("notificationType", notification.getType()); // Ensure this key is set + startActivity(intent); } - -} +} \ No newline at end of file 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 a680743..62c6093 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; @@ -11,13 +12,19 @@ import com.example.vortex_app.controller.adapter.OrgWaitingListAdapter; 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 { @@ -82,6 +89,11 @@ private void fetchWaitingList(String eventID) { }); } + /** + * 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() { db.collection("events") .document(eventID) @@ -97,15 +109,64 @@ private void selectAndStoreUsers() { List shuffledList = new ArrayList<>(waitingListEntrants); Collections.shuffle(shuffledList); List selectedUsers = shuffledList.subList(0, usersToSelect); + List nonSelectedUsers = shuffledList.subList(usersToSelect, shuffledList.size()); + WriteBatch batch = db.batch(); + + // Process selected users for (User user : selectedUsers) { + String userID = user.getUserID(); // Save the User object directly to the Firestore db.collection("selected_but_not_confirmed") .add(user) .addOnSuccessListener(documentReference -> removeFromWaitlisted(user)) .addOnFailureListener(e -> Toast.makeText(this, "Failed to store selected user", Toast.LENGTH_SHORT).show()); + + // 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"); + selectedNotificationData.put("notificationType", "selected"); + + // storing in notifications collection using batch operations + DocumentReference selectedNotificationRef = db.collection("notifications").document(); + batch.set(selectedNotificationRef, selectedNotificationData); + } + + // 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"); + lostNotificationData.put("notificationType", "lost"); + + DocumentReference lostNotificationRef = db.collection("notifications").document(); + batch.set(lostNotificationRef, lostNotificationData); } + // Commit the batch for notifications + batch.commit() + .addOnSuccessListener(aVoid -> { + Toast.makeText(this, "Notifications sent successfully.", Toast.LENGTH_SHORT).show(); + }) + .addOnFailureListener(e -> { + Toast.makeText(this, "Failed to send notifications.", Toast.LENGTH_SHORT).show(); + Log.e("OrgWaitingListActivity", "Error committing batch: ", e); + }); + + // Refresh the waiting list + fetchWaitingList(eventID); Toast.makeText(this, "Selected users stored successfully.", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "No users to select or invalid maxPeople.", Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/res/drawable/ic_acceptt.xml b/app/src/main/res/drawable/ic_acceptt.xml new file mode 100644 index 0000000..9a1521a --- /dev/null +++ b/app/src/main/res/drawable/ic_acceptt.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_decline.xml b/app/src/main/res/drawable/ic_decline.xml new file mode 100644 index 0000000..ad9f9f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_decline.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/notification.xml b/app/src/main/res/drawable/notification.xml index fd1545f..ce88999 100644 --- a/app/src/main/res/drawable/notification.xml +++ b/app/src/main/res/drawable/notification.xml @@ -1,5 +1,6 @@ + - + - + diff --git a/app/src/main/res/layout/activity_notification_detail.xml b/app/src/main/res/layout/activity_notification_detail.xml index 0405b59..021c688 100644 --- a/app/src/main/res/layout/activity_notification_detail.xml +++ b/app/src/main/res/layout/activity_notification_detail.xml @@ -1,10 +1,11 @@ - - + android:layout_height="match_parent"> + + app:layout_constraintEnd_toEndOf="parent"> + android:layout_marginBottom="8dp" + android:background="#DDDDDD" /> + android:lineSpacingExtra="4dp" /> - \ No newline at end of file + + + + +