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());
+ }
+}