Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@

</provider>

<receiver android:name="com.google.android.samples.socialite.ReplyReceiver" />
<receiver android:name=".ReplyReceiver" />
<receiver android:name=".fcm.MessagingBroadcastReceiver" />

<!-- Trigger Google Play services to install the backported photo picker module.
Refer https://developer.android.com/training/data-storage/shared/photopicker#device-availability -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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}",
)
Comment on lines +55 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

The Log.d call on line 57 logs the entire FCM data payload, which can contain sensitive Personally Identifiable Information (PII). Logging PII to system logs is a security risk as it can be accessed by other applications or adb logcat. It is crucial to sanitize logs to prevent data leakage. Additionally, the overall message processing logic (lines 45-68) should be reviewed to ensure remoteMessage.notification payloads are handled independently of remoteMessage.data to avoid missed notifications, as currently, notification payloads might not be processed if the data payload is empty.

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.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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].
*/
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
Loading