From d3a69b7aeae76333553eadf7259d3cb1919108e1 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:41:54 -0600 Subject: [PATCH] added push notification when a new conversation is made --- android/app/src/main/AndroidManifest.xml | 6 ++ .../petstoremobile/PetStoreApplication.java | 2 - .../activities/HomeActivity.java | 75 +++++++++++++--- .../fragments/ProfileFragment.java | 5 ++ .../services/ChatNotificationService.java | 88 +++++++++++++++++++ .../utils/NotificationHelper.java | 53 +++++++++++ 6 files changed, 216 insertions(+), 13 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/utils/NotificationHelper.java diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 633ea6ba..cef8c8c2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> + + + + requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (!isGranted) { + Log.w("HomeActivity", "Notification permission denied"); + } + }); @Override protected void onCreate(Bundle savedInstanceState) { @@ -34,17 +49,15 @@ public class HomeActivity extends AppCompatActivity { }); //get the bottom navbar from the layout - BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation); - - // Load ListFragment by default only if this is a fresh start + bottomNav = findViewById(R.id.bottom_navigation); + + //load the list fragment by default if it's a fresh start if (savedInstanceState == null) { - loadFragment(new ListFragment()); - bottomNav.setSelectedItemId(R.id.nav_list); + handleIntent(getIntent()); } - //when an item in the bar is selected, load the corresponding fragment + //when an item in the bottom bar is selected, load the corresponding fragment bottomNav.setOnItemSelectedListener(item -> { - if (item.getItemId() == R.id.nav_list) { loadFragment(new ListFragment()); return true; @@ -57,13 +70,53 @@ public class HomeActivity extends AppCompatActivity { } return false; }); + + // Start the notification service and request for notification permission + startNotificationService(); + requestNotificationPermission(); + } + + // Handle new intents when the activity is already running, + // like clicking a notification while the app is in use + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleIntent(intent); } - //helper function to load a fragment + // Helper function to process intents for navigation. + // like clicking a notification or just launching the app from a fresh start + private void handleIntent(Intent intent) { + if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) { + loadFragment(new ChatFragment()); + bottomNav.setSelectedItemId(R.id.nav_chat); + } else { + loadFragment(new ListFragment()); + bottomNav.setSelectedItemId(R.id.nav_list); + } + } + + // Helper function to start the notification service in the background + // to receive notifications when a new conversation is created + private void startNotificationService() { + Intent serviceIntent = new Intent(this, ChatNotificationService.class); + startService(serviceIntent); + } + + //Helper function to request for notification permission + private void requestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + } + + //Helper function to load a fragment private void loadFragment(Fragment fragment) { getSupportFragmentManager() .beginTransaction() .replace(R.id.fragment_container, fragment) .commit(); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index a5d132c3..e754b7fa 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -33,6 +33,7 @@ import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.dtos.ErrorResponse; import com.example.petstoremobile.dtos.UserDTO; +import com.example.petstoremobile.services.ChatNotificationService; import com.example.petstoremobile.utils.InputValidator; import com.google.gson.Gson; @@ -229,6 +230,10 @@ public class ProfileFragment extends Fragment { //Logout button btnLogout.setOnClickListener(v -> { + // Stop notification service before logging out so notifications stop + Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class); + requireContext().stopService(serviceIntent); + TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login //get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen Intent intent = new Intent(getActivity(), MainActivity.class); diff --git a/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java new file mode 100644 index 00000000..fadd1886 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/services/ChatNotificationService.java @@ -0,0 +1,88 @@ +package com.example.petstoremobile.services; + +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.util.Log; +import androidx.annotation.Nullable; +import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.utils.NotificationHelper; +import com.example.petstoremobile.websocket.StompChatManager; +import java.util.HashSet; +import java.util.Set; + +// Service to receive notifications when a new conversation is created +public class ChatNotificationService extends Service { + private static final String TAG = "ChatNotificationService"; + private StompChatManager stompChatManager; + private final Set knownConversationIds = new HashSet<>(); + + //When the service starts, connect to the websocket + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "Service started"); + connectWebSocket(); + return START_STICKY; + } + + // helper function to connect to the websocket + private void connectWebSocket() { + //get the token and role from the shared preferences + TokenManager tm = TokenManager.getInstance(this); + String token = tm.getToken(); + String role = tm.getRole(); + + + if (token != null && stompChatManager == null) { + stompChatManager = new StompChatManager(token, role); + + //When a conversation gets created, show a notification + stompChatManager.setConversationListener(conversation -> { + //check if the conversation exists + if (conversation != null && conversation.getId() != null) { + //check if the conversation is new + if (!knownConversationIds.contains(conversation.getId())) { + //add the conversation to the set of known conversations + knownConversationIds.add(conversation.getId()); + NotificationHelper.showNotification( + getApplicationContext(), + "Customer Support", + "A customer request for assistance" + ); + + } + } + }); + stompChatManager.setConnectionListener(new StompChatManager.ConnectionListener() { + // when the websocket is connected, set isFirstLoad to false after a delay + @Override + public void onSocketOpened() { + Log.d(TAG, "WebSocket connected in service"); + } + + @Override + public void onSocketClosed() { Log.d(TAG, "WebSocket closed in service"); } + + @Override + public void onSocketError() { Log.e(TAG, "WebSocket error in service"); } + }); + stompChatManager.connect(); + } + } + + //When the service is destroyed, disconnect from the websocket + @Override + public void onDestroy() { + if (stompChatManager != null) { + stompChatManager.disconnect(); + } + super.onDestroy(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/NotificationHelper.java b/android/app/src/main/java/com/example/petstoremobile/utils/NotificationHelper.java new file mode 100644 index 00000000..7ec80cb7 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/NotificationHelper.java @@ -0,0 +1,53 @@ +package com.example.petstoremobile.utils; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import androidx.core.app.NotificationCompat; +import com.example.petstoremobile.R; +import com.example.petstoremobile.activities.HomeActivity; + +// Helper class to show notifications when called +public class NotificationHelper { + private static final String CHANNEL_ID = "chat_notifications"; + private static final String CHANNEL_NAME = "Chat Notifications"; + private static final String CHANNEL_DESC = "Notifications for new conversations"; + private static final int NOTIFICATION_ID = 1; + + // a function to show a notification + public static void showNotification(Context context, String title, String message) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + //check if the device is running on Oreo or higher so we can set up a notification channel + // for these devices + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // Create a notification channel + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); + channel.setDescription(CHANNEL_DESC); + notificationManager.createNotificationChannel(channel); + } + + //make the notification navigate the chat if it is clicked + Intent intent = new Intent(context, HomeActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra("navigate_to", "chat"); + + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + + //build the notification for display + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(title) + .setContentText(message) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setDefaults(NotificationCompat.DEFAULT_ALL) + .setAutoCancel(true) + .setContentIntent(pendingIntent); + + notificationManager.notify(NOTIFICATION_ID, builder.build()); + } +}