diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f813a705..09d4f764 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -182,7 +182,8 @@
-
+
+
diff --git a/app/src/main/java/com/google/android/samples/socialite/MainActivity.kt b/app/src/main/java/com/google/android/samples/socialite/MainActivity.kt
index 5a661ec7..1e5a449c 100644
--- a/app/src/main/java/com/google/android/samples/socialite/MainActivity.kt
+++ b/app/src/main/java/com/google/android/samples/socialite/MainActivity.kt
@@ -70,10 +70,12 @@ class MainActivity : ComponentActivity() {
if (!task.isSuccessful) {
Log.w("FCM", "Fetching FCM registration token failed", task.exception)
return@OnCompleteListener
- } // Get new FCM registration token
+ } // Get new FCM registration token.
+ // A unique key for this specific app instance and used for sending messages to
+ // this specific app instance.
val token = task.result
// Log token, for testing purposes only.
- // Log.d("FCM", "FCM message token $token")
+ // Log.d(MainActivity::class::simpleName.toString(), "FCM message token $token")
},
)
}
diff --git a/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingBroadcastReceiver.kt b/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingBroadcastReceiver.kt
new file mode 100644
index 00000000..34a4811d
--- /dev/null
+++ b/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingBroadcastReceiver.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2026 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.samples.socialite.fcm
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.google.android.samples.socialite.repository.NOTIFICATION_DISMISSED
+import com.google.android.samples.socialite.repository.NOTIFICATION_ID
+
+/**
+ * Receiver for handling notification dismissals set via
+ * Notification$Builder#setDeleteIntent(Intent).
+ *
+ * In a multi-device world (mobile, large screen), notification
+ * dismissal on one client should propagate to all other clients
+ * to dismiss the notification.
+ */
+class MessagingBroadcastReceiver : BroadcastReceiver() {
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == NOTIFICATION_DISMISSED) {
+ intent.extras?.getInt(NOTIFICATION_ID)?.let {
+ Log.d(
+ MessagingBroadcastReceiver::class::simpleName.toString(),
+ "Dismissing notification w/ ID $it",
+ )
+ // Here is where your app server is notified of a notification dismissal.
+ // The app server can then talk to FCM backend to route a message to
+ // all clients to dismiss/cancel the notification.
+ // See sending messages to devices in
+ // https://firebase.google.com/docs/cloud-messaging/server-environment
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingService.kt b/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingService.kt
index 9937c2a3..7834b788 100644
--- a/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingService.kt
+++ b/app/src/main/java/com/google/android/samples/socialite/fcm/MessagingService.kt
@@ -16,9 +16,17 @@
package com.google.android.samples.socialite.fcm
+import android.app.NotificationManager
+import android.util.Log
+import androidx.core.content.getSystemService
+import com.google.android.samples.socialite.repository.NOTIFICATION_ACTION
+import com.google.android.samples.socialite.repository.NOTIFICATION_ID
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
+/**
+ * Service for handling Firebase Cloud Messaging.
+ */
class MessagingService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
@@ -30,15 +38,43 @@ class MessagingService : FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
+ val notificationManager: NotificationManager =
+ this.getSystemService() ?: throw IllegalStateException()
+
// Handle data payload
if (remoteMessage.data.isNotEmpty()) {
- // Log.d("FCM", "Message data payload: ${remoteMessage.data}")
- }
+ // If payload is relevant to dismissal, cancel the notification.
+ // For the device that triggered this dismissal, it is noop but
+ // for other devices that belongs to the user, the notification will be
+ // removed.
+ // Note that the below is just an example of a payload that can be
+ // relevant to dismissal.
+ if (remoteMessage.data[NOTIFICATION_ACTION] == "DISMISSAL" &&
+ remoteMessage.data[NOTIFICATION_ID] != null
+ ) {
+ Log.d(
+ MessagingService::class::simpleName.toString(),
+ "Message data payload: ${remoteMessage.data}",
+ )
+ remoteMessage.data[NOTIFICATION_ID]?.toIntOrNull()
+ ?.let { notificationManager.cancel(it) }
+ }
- // Handle notification payload
- remoteMessage.notification?.let {
- // Log.d("FCM", "Message Notification Body: ${it.body}")
- // Trigger local notification here
+ // Handle notification payload
+ remoteMessage.notification?.let {
+ // Log.d("FCM", "Message Notification Body: ${it.body}")
+ // Trigger local notification here
+ // This notif should have unique ID as well.
+ }
}
}
+
+ /**
+ * It is good practice to handle deleted messages in FCM
+ * See https://firebase.google.com/docs/cloud-messaging/android/receive-messages#override-on-deleted-messages
+ */
+ override fun onDeletedMessages() {
+ super.onDeletedMessages()
+ // Up to the developer on how to handle this and surface this behavior to the end user.
+ }
}
diff --git a/app/src/main/java/com/google/android/samples/socialite/repository/NotificationHelper.kt b/app/src/main/java/com/google/android/samples/socialite/repository/NotificationHelper.kt
index ed8ff31f..161d7380 100644
--- a/app/src/main/java/com/google/android/samples/socialite/repository/NotificationHelper.kt
+++ b/app/src/main/java/com/google/android/samples/socialite/repository/NotificationHelper.kt
@@ -41,6 +41,7 @@ import com.google.android.samples.socialite.BubbleActivity
import com.google.android.samples.socialite.MainActivity
import com.google.android.samples.socialite.R
import com.google.android.samples.socialite.ReplyReceiver
+import com.google.android.samples.socialite.fcm.MessagingBroadcastReceiver
import com.google.android.samples.socialite.model.Contact
import com.google.android.samples.socialite.model.Message
import dagger.hilt.android.qualifiers.ApplicationContext
@@ -55,6 +56,10 @@ enum class PushReason {
OutgoingMessage,
}
+const val NOTIFICATION_ACTION = "NOTIFICATION_ACTION"
+const val NOTIFICATION_DISMISSED = "NOTIFICATION_DISMISSED"
+const val NOTIFICATION_ID = "NOTIFICATION_ID"
+
/**
* Handles all operations related to [Notification].
*/
@@ -197,6 +202,9 @@ class NotificationHelper @Inject constructor(@ApplicationContext context: Contex
}
}
+ val lastMessage = messages.last()
+ val notificationId = contact.id.toInt() + lastMessage.id.toInt()
+
val builder = NotificationCompat.Builder(appContext, CHANNEL_NEW_MESSAGES)
// A notification can be shown as a bubble by calling setBubbleMetadata()
.setBubbleMetadata(
@@ -268,12 +276,25 @@ class NotificationHelper @Inject constructor(@ApplicationContext context: Contex
// Let's add some more content to the notification in case it falls back to a normal
// notification.
.setStyle(messagingStyle)
- .setWhen(messages.last().timestamp)
+ .setWhen(lastMessage.timestamp)
+ .setDeleteIntent(createNotificationDismissIntent(notificationId))
// Don't sound/vibrate if an update to an existing notification.
if (update) {
builder.setOnlyAlertOnce(true)
}
- notificationManager.notify(contact.id.toInt(), builder.build())
+ notificationManager.notify(notificationId, builder.build())
+ }
+
+ private fun createNotificationDismissIntent(notificationId: Int): PendingIntent {
+ val deleteIntent = Intent(appContext, MessagingBroadcastReceiver::class.java)
+ deleteIntent.action = NOTIFICATION_DISMISSED
+ deleteIntent.putExtra(NOTIFICATION_ID, notificationId)
+ return PendingIntent.getBroadcast(
+ appContext,
+ notificationId,
+ deleteIntent,
+ flagUpdateCurrent(mutable = false),
+ )
}
fun dismissNotification(chatId: Long) {