Introduction
This guide provides a complete walkthrough for handling Firebase Cloud Messaging (FCM) notifications for chat messages and VoIP-based calling in an Android app. It focuses on managing chat and VoIP call notifications effectively, ensuring users receive real-time alerts for messages and incoming calls whether the app is in the foreground, background, or killed.
Prerequisites
Before proceeding, ensure that your Firebase Console is properly configured with a service account for server-side authentication, and that push notifications are enabled in your CometChat Dashboard. These setups are essential for receiving FCM-based chat and call notifications.
Follow the official CometChat guide: Push Notification Setup
Setting up Dependencies
Add the following dependencies in your build.gradle (app-level) file:
implementation(platform("com.google.firebase:firebase-bom:33.16.0"))
implementation("com.google.firebase:firebase-messaging:23.4.1")
implementation("com.cometchat:chat-sdk-android:4.0.12")
implementation("com.cometchat:calls-sdk-android:4.1.0")
Setting up Permissions
In Manifest:
To use the Telecom API and receive VoIP calls, you must declare the required permissions.
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
Runtime Permission Request:
You need to request for some permissions at runtime. Here’s how you can request them:
val requiredPermissions = arrayOf(
Manifest.permission.READ_PHONE_NUMBERS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.CALL_PHONE,
)
val missingPermissions = requiredPermissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (missingPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(this, missingPermissions.toTypedArray(), PHONE_PERMISSION_CODE)
}
Prompting User to Enable Calling Account
To allow the app to handle VoIP calls, the user must enable the app as a calling account. You can check this and prompt the user to enable the account using the following method.
private fun checkIfPhoneAccountEnabled() {
val telecomManager = getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val componentName = ComponentName(this, CallConnectionService::class.java)
val handle = PhoneAccountHandle(componentName, CometChatNotification.ACCOUNT_ID)
val account = telecomManager.getPhoneAccount(handle)
if (account != null && !account.isEnabled) {
AlertDialog.Builder(this)
.setTitle("Enable Calling Feature")
.setMessage("To receive calls, please enable our application as a calling account.")
.setPositiveButton("Go to Settings") { _, _ ->
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
startActivity(intent)
}
.setNegativeButton("Cancel", null)
.show()
}
}
Tip: Call this in your Application class or main activity’s onCreate() so the user is prompted as soon as the app launches.
Files Overview
Here are the core files used for handling VoIP call flow:
File | Description |
---|---|
PushNotificationService.kt | Handles Firebase push messages and routes to the proper service. |
CallConnectionService.kt | Foreground service for managing the lifecycle of the calls. |
CallConnection.kt | Implements the connection logic for the telecom framework. |
CometChatNotification.kt | Helper class to register phone accounts and trigger system call UI. |
Setting up Helper Object (CometChatNotification.kt)
This Helper object is used for Phone Account Registration and prompting the TelecomManger function.
object CometChatNotification {
const val ACCOUNT_ID = "comet_account_id"
fun registerPhoneAccount(context: Context) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val componentName = ComponentName(context, CallConnectionService::class.java)
val handle = PhoneAccountHandle(componentName, ACCOUNT_ID)
val phoneAccount = PhoneAccount.builder(handle, "VoIP Calls")
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.setHighlightColor(android.graphics.Color.BLUE)
.setAddress(Uri.fromParts(PhoneAccount.SCHEME_TEL, "cometchat", null))
.build()
telecomManager.registerPhoneAccount(phoneAccount)
}
fun showIncomingCall(context: Context, callerName: String, callType: String, sessionId: String) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
val handle = PhoneAccountHandle(
ComponentName(context, CallConnectionService::class.java),
ACCOUNT_ID
)
val extras = Bundle().apply {
putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, Uri.fromParts("tel", callerName, null))
}
telecomManager.addNewIncomingCall(handle, extras)
}
}
Phone Account Register
It’s important to register the phone account at multiple points, like when the app launches or when an FCM service is created, because Android may silently remove the phone account if the app is force-stopped or affected by battery optimizations. Registering it in Application.onCreate() ensures it’s ready as soon as the app starts, and doing it again inside PushNotificationService makes sure it’s in place before showing any incoming call UI.
//Note: Place this code in the onCreate() function of MainActivity or the Application class.
CometChatNotification.registerPhoneAccount(this)
Setting up Firebase Messaging Service (PushNotificationService.kt)
Declare Service in Manifest
Add the FCM service using the service tag under the application.
<service
android:name=".PushNotificationService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Token Management
When a new FCM token is received, register it with CometChat and store it (e.g., in SharedPreferences) for reuse after login. This ensures push notifications work correctly. Refer to CometChat’s FCM token guide for details.
override fun onNewToken(token: String) {
super.onNewToken(token)
val user = CometChat.getLoggedInUser()
if (user != null) {
//User is currently logged In
//Register the token using CometChatNotifications.registerPushToken
} else {
//User is not logged in yet, so store the token to register later
val prefs = getSharedPreferences("cometchat_notify", MODE_PRIVATE)
prefs.edit().putString("pending_token", token).apply()
}
}
Register phone account
Register the phone account inside the FCMService when it’s initialized (i.e., in its onCreate() method).
override fun onCreate() {
super.onCreate()
CometChatNotification.registerPhoneAccount(this)
}
Handle FCM Notifications in onMessageReceived()
The chat and call notification logic is handled within the onMessageReceived() method, as this is where incoming FCM messages are received.
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
if (remoteMessage.data.isNotEmpty()) {
try {
when (remoteMessage.data["type"]) {
"chat" -> {
if (!CometChat.isInitialized()) {
// Initialize CometChat
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
AppConstants.channel_id,
AppConstants.channel_name,
NotificationManager.IMPORTANCE_HIGH
)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
val intent = Intent(this, YourActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
val notification = NotificationCompat.Builder(this, AppConstants.channel_id)
.setSmallIcon(R.drawable.logo)
.setColor(resources.getColor(R.color.std_green, null))
.setColorized(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
getSystemService(NotificationManager::class.java).notify(Random.nextInt(), notification)
}
"call" -> {
if (remoteMessage.data["callAction"] == "initiated") {
val callerName = remoteMessage.data["senderName"]
val callType = remoteMessage.data["body"]
val sessionId = remoteMessage.data["sessionId"]
CometChatNotification.showIncomingCall(
this,
callerName.orEmpty(),
callType.orEmpty(),
sessionId.orEmpty()
)
}
}
}
} catch (e: Exception) {
Log.e("PushService", "Error handling push", e)
}
}
}
Setting up ConnectionService (CallConnectionService.kt)
Declare Service in Manifest:
Add the Connection service using the service tag under the application.
<service
android:name=".services.CallConnectionService"
android:exported="true"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
Override the onCreateIncomingConnection method:
Here, a CallConnection object is instantiated and configured with the necessary attributes such as caller name and call type. The connection state is updated through initialization, ringing, and activation before returning the connection instance.
class CallConnectionService : ConnectionService() {
override fun onCreateIncomingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle,
request: ConnectionRequest
): Connection {
val callerUri = request.address
val callerName = callerUri?.schemeSpecificPart ?: "Unknown"
val callType = request.extras?.getString("callType")
val sessionId = request.extras?.getString("sessionId")
val connection = CallConnection(this, sessionId ?: "")
connection.setCallerDisplayName(callerName, TelecomManager.PRESENTATION_ALLOWED)
connection.setAddress(Uri.parse("cometchat:$callType"), TelecomManager.PRESENTATION_ALLOWED)
connection.setInitializing()
connection.setRinging()
connection.setActive()
return connection
}
}
Setting up Call Connection (CallConnection.kt)
Handle the logic for the “Accept” and “Reject” buttons in a native VoIP calling notification.
class CallConnection(private val context: Context,
private val sessionId: String) : Connection() {
override fun onAnswer() {
super.onAnswer()
Log.d("CallConnection", "Call answered")
//For Killed State
if (!CometChat.isInitialized()) {
//Initialize CometChat using CometChat.init() and CometChatCalls.init()
}
//Handle the logic for accepting the call
//Tip: Call the CometChat.acceptCall() using the sessionId to accept the call
}
override fun onReject() {
super.onReject()
Log.d("CallConnection", "Call rejected")
//Tip: Call the CometChat.rejectCall() using the sessionId to reject the call
//You may disconnect the call connection here, or handle it based on your use case.
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
}
}
Reference
GitHub Repo: cometchat-push-notification