diff --git a/notifications/android-push-notifications.mdx b/notifications/android-push-notifications.mdx index 33b0d5a59..9523d313d 100644 --- a/notifications/android-push-notifications.mdx +++ b/notifications/android-push-notifications.mdx @@ -1,711 +1,277 @@ --- title: "Android" -description: "Setup FCM and CometChat for message and call push notifications on Android." +description: "Add CometChat push notifications and VoIP calls to an Android app with the drop-in cometchat push-notifications-android SDK." --- - - - Reference implementation of Kotlin UI Kit, FCM and Push Notification Setup. - ## What this guide covers -- FCM setup and CometChat provider wiring (credentials + Gradle + manifest). -- Token registration/unregistration so CometChat routes pushes correctly. -- Handling message pushes with grouped notifications and inline reply. -- Handling call pushes with `ConnectionService` for native telecom UI. -- Deep links/navigation from notifications and payload customization. -- App icon badge count and grouped notifications using `unreadMessageCount` from the CometChat push payload. +- Adding the `push-notifications-android` SDK and initializing it. +- Wiring your own Firebase Messaging service to forward payloads to the SDK. +- Requesting permission and registering the FCM token after login. +- Letting the SDK render chat notifications (grouped, inline reply) and full-screen VoIP calls. +- Handling notification taps and call events, and suppressing notifications for the open chat. +- Testing and troubleshooting. -{/* ## What you need first + +The `push-notifications-android` SDK replaces the previous approach of copying the UI Kit sample's `fcm/` and `voip/` packages (`FCMService`, `FCMMessageNotificationUtils`, `FCMMessageBroadcastReceiver`, `CometChatVoIP`, `CometChatVoIPConnectionService`, and related helpers). Token registration, notification channels, stacking, avatars, inline reply, delivery receipts, the VoIP ConnectionService, the incoming-call screen, ring timeout, call-collision handling, and SDK auto-init on a killed-app wake are all handled inside the SDK. Your app keeps only a ~5-line Firebase Messaging service and three facade calls. + -- Firebase project with an Android app added, `google-services.json` downloaded, and Cloud Messaging enabled. -- CometChat App ID, Region, Auth Key; **Push Notifications** enabled with an **FCM Android provider** and its Provider ID. -- Android device with Play Services (sample uses `minSdk 26` because of `ConnectionService`). -- Latest CometChat UI Kit + Calls SDK dependencies (see Gradle tabs below). */} +## How it works -## How FCM and CometChat fit together +- **FCM's role:** Firebase issues the registration token and delivers the CometChat payload as a **data message**. The SDK does **not** depend on `firebase-messaging` — your app owns Firebase and passes the payload as a `Map`. +- **CometChat's role:** The FCM provider you add in the dashboard binds your registered token to the logged-in user so CometChat can route pushes on your behalf. +- **The SDK's role:** `CometChatPushNotifications` parses the payload and decides chat vs call. Chat → builds/stacks notifications. Call → drives a full-screen incoming-call experience over an Android Telecom `SELF_MANAGED` `ConnectionService` (the same approach WhatsApp/Signal use — no `READ_PHONE_STATE`/`ANSWER_PHONE_CALLS` and no phone-account toggle in Settings). +- **Killed-app wake:** When FCM starts your process, `init()` in `Application.onCreate()` runs first and stores your credentials; `handlePushNotification(...)` then auto-initializes the Chat SDK before routing, so calls connect even from a terminated state. -- **Why FCM?** Google issues device tokens and delivers raw push payloads to Android. You must add `google-services.json`, the Messaging SDK, and a service receiver (`FCMService`) so the device can receive pushes. -- **Why a CometChat provider?** The Provider ID tells CometChat which FCM credentials to use when sending to your app. Without registering tokens against this ID, CometChat cannot target your device. -- **Token registration bridge:** The app retrieves the FCM token and calls `CometChatNotifications.registerPushToken(pushToken, PushPlatforms.FCM_ANDROID, providerId, …)`. That binds the token to your logged-in user so CometChat can route message/call pushes to FCM on your behalf. -- **Payload handling:** When FCM delivers a push, your `FCMService`/`FCMMessageBroadcastReceiver` parses CometChat’s payload, shows notifications (grouped, inline reply), and forwards intents to your activities. For calls, `CometChatVoIPConnectionService` surfaces a telecom-grade UI and uses the same payload to accept/reject server-side. -- **Dashboard ↔ app contract:** The Provider ID in `AppConstants.FCMConstants.PROVIDER_ID` must match the dashboard provider you created. The package name in Firebase and the `applicationId` in Gradle must match, or FCM will reject the token. +## Prerequisites + +- The FCM provider, Firebase project, and `google-services.json` from **[Getting Started](/notifications/push-getting-started)** (this guide assumes those are done). +- The CometChat **Chat SDK / UI Kit** already integrated (you call `CometChatUIKit.init()` / `login()` yourself). +- **minSdk 24+**, **compileSdk 36**, **Java 11**, and a Gradle/AGP version matching your UI Kit project. +- A physical device — background delivery and full-screen calls are unreliable on emulators. **Complete the [Getting Started](/notifications/push-getting-started) guide first** — enable Push Notifications, add your FCM provider, and configure Firebase (`google-services.json` + service account JSON). This guide covers only the Android app wiring. -## 1. Add dependencies (Gradle) - -Use a version catalog and aliases (Update `applicationId`, package names, icons, and app name.). Also, if you are new to CometChat, please review the Maven repositories and related setup requirements before proceeding. - - - - -```toml lines -[versions] -minSdk = "26" -compileSdk = "35" -targetSdk = "35" -agp = "8.7.0" -kotlin = "2.0.0" -googleServices = "4.4.2" -cometChatUikit = "5.2.6" -cometChatSdk = "4.1.8" -cometChatCalls = "4.3.2" -firebaseBom = "33.7.0" -coreKtx = "1.13.1" -appcompat = "1.7.0" -material = "1.12.0" -gson = "2.11.0" -glide = "4.16.0" - -[libraries] -cometchat-uikit = { group = "com.cometchat", name = "chat-uikit", version.ref = "cometChatUikit" } -cometchat-sdk = { group = "com.cometchat", name = "chat-sdk-android", version.ref = "cometChatSdk" } -cometchat-calls = { group = "com.cometchat", name = "calls-sdk-android", version.ref = "cometChatCalls" } -firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } -firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } -firebase-auth = { group = "com.google.firebase", name = "firebase-auth" } -androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } -androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } -material = { group = "com.google.android.material", name = "material", version.ref = "material" } -gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } -glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" } - -[plugins] -android-application = { id = "com.android.application", version.ref = "agp" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } -``` - -This TOML file defines versions and aliases for the required dependencies. - - - -```gradle lines -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) - alias(libs.plugins.google.services) -} +## 1. Add the dependency -android { - compileSdk 35 - defaultConfig { - applicationId "your.package.name" - minSdk 26 - targetSdk 35 - } - kotlinOptions { jvmTarget = "11" } -} +Add the CometChat Maven repository (the same one that serves the Chat and Calls SDKs) in `settings.gradle.kts`: -dependencies { - // CometChat - implementation(libs.cometchat.uikit) - implementation(libs.cometchat.sdk) - implementation(libs.cometchat.calls) - - // Firebase - implementation(platform(libs.firebase.bom)) - implementation(libs.firebase.messaging) - implementation(libs.firebase.auth) - - // UI + utilities - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) - implementation(libs.gson) - implementation(libs.glide) +```kotlin settings.gradle.kts +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("https://dl.cloudsmith.io/public/cometchat/cometchat/maven/") + } } ``` - - - -- Apply the `google-services` plugin and place `google-services.json` in the same module; keep `viewBinding` enabled if you copy UI Kit screens directly from the sample. -- Update `applicationId`, package names, icons, and app name as needed. - -## 2. Manifest permissions and services - -Start from the sample [`AndroidManifest.xml`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/AndroidManifest.xml): - -```xml lines highlight={15, 19, 26, 29} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -- Permissions cover notifications + telecom; services/receiver wire Firebase delivery (`FCMService`), notification actions (`FCMMessageBroadcastReceiver`), and telecom UI (`CometChatVoIPConnectionService`). Point `android:name` to your `MyApplication`. +Add the SDK plus your own Firebase Messaging dependency in the app module. Apply the `google-services` plugin so `google-services.json` is picked up: -- Set `android:name` on `` to your `MyApplication` subclass. -- Keep runtime permission prompts for notifications, mic, camera, and media access (see `AppUtils.kt` / `HomeActivity.kt` in the sample). - -## 3. Application wiring, sample code, and callbacks - -- Clone/open the [reference repo](https://github.com/cometchat/cometchat-uikit-android/tree/v5/sample-app-kotlin%2Bpush-notification). -- Copy into your app module (keep structure): - - [`fcm/fcm`](https://github.com/cometchat/cometchat-uikit-android/tree/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm) for services/DTOs/notification utils/broadcast receiver. - - [`fcm/voip`](https://github.com/cometchat/cometchat-uikit-android/tree/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/voip) for ConnectionService + VoIP helpers. - - `fcm/utils` for `MyApplication`, `AppUtils`, `AppConstants`, `AppCredentials`. - - Copy String values from `res/values/strings.xml`. - - BuildConfig file `build.gradle`. -- Update packages to your namespace; set `AppCredentials` (App ID/Auth Key/Region) and `AppConstants.FCMConstants.PROVIDER_ID` to your dashboard provider. Point `` and services/receivers to your package; update app name/icons as needed. -- Keep notification constants from [`AppConstants.kt`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/utils/AppConstants.kt); rename channels/keys consistently if you change them. - -**What the core pieces do** - -- `FCMService` – receives FCM data/notification messages, parses CometChat payload, and hands off to `FCMMessageBroadcastReceiver`. -- `FCMMessageBroadcastReceiver` – builds grouped notifications, inline reply actions, and routes taps/deeplinks to your `HomeActivity`. -- `Repository.registerFCMToken` – fetches the FCM token and registers it with CometChat using `AppConstants.FCMConstants.PROVIDER_ID`; call after login. -- `Repository.acceptCall/rejectCall/rejectCallWithBusyStatus` – performs server-side call actions so the caller sees the correct state even if your UI is backgrounded. -- `MyApplication` – initializes UIKit, manages websocket connect/disconnect, tracks foreground state, and shows/dismisses incoming call overlays. -- `CometChatVoIPConnectionService` – handles Android telecom integration so call pushes display a system-grade incoming call UI and cleanly end/busy on reject. - -**Splash/entry deep link handler** (adapt activity targets): - -```kotlin lines -// In your Splash/entry activity (e.g., SplashActivity) -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - handleDeepLinking() +```kotlin app/build.gradle.kts +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("com.google.gms.google-services") } -private fun handleDeepLinking() { - NotificationManagerCompat.from(this) - .cancel(AppConstants.FCMConstants.NOTIFICATION_GROUP_SUMMARY_ID) - - val notificationType = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE) - val notificationPayload = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD) +dependencies { + implementation("com.cometchat:push-notifications-android:1.0.0") - startActivity( - Intent(this, HomeActivity::class.java).apply { - putExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE, notificationType) - putExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD, notificationPayload) - } - ) - finish() + // Your app owns Firebase — the SDK does not depend on it. + implementation(platform("com.google.firebase:firebase-bom:34.15.0")) + implementation("com.google.firebase:firebase-messaging") } ``` -This reads the push extras, clears the summary notification, and forwards the payload to `HomeActivity` so taps or deep links land in the right screen. - -**SplashViewModel (init UIKit + login check)** - -```kotlin lines -class SplashViewModel : ViewModel() { - private val loginStatus = MutableLiveData() - - fun initUIKit(context: Context) { - val appId = AppUtils.getDataFromSharedPref(context, String::class.java, R.string.app_cred_id, AppCredentials.APP_ID) - val region = AppUtils.getDataFromSharedPref(context, String::class.java, R.string.app_cred_region, AppCredentials.REGION) - val authKey = AppUtils.getDataFromSharedPref(context, String::class.java, R.string.app_cred_auth, AppCredentials.AUTH_KEY) + +You do **not** need to add notification, full-screen-intent, foreground-service, `WAKE_LOCK`, or `MANAGE_OWN_CALLS` permissions, or declare the incoming-call activity / VoIP services in your `AndroidManifest.xml`. The SDK's manifest declares everything it needs and Gradle merges it into your app automatically. + - val uiKitSettings = UIKitSettings.UIKitSettingsBuilder() - .setAutoEstablishSocketConnection(false) - .setAppId(appId) - .setRegion(region) - .setAuthKey(authKey) - .subscribePresenceForAllUsers() - .build() - - CometChatUIKit.init(context, uiKitSettings, object : CometChat.CallbackListener() { - override fun onSuccess(s: String) { - CometChat.setDemoMetaInfo(getAppMetadata(context)) - checkUserIsNotLoggedIn() - } - override fun onError(e: CometChatException) { - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - } - }) - } +## 2. Store your credentials - private fun getAppMetadata(context: Context): JSONObject { - val jsonObject = JSONObject() - jsonObject.put("name", context.getString(R.string.app_name)) - jsonObject.put("bundle", BuildConfig.APPLICATION_ID) - jsonObject.put("version", BuildConfig.VERSION_NAME) - jsonObject.put("platform", "android") - return jsonObject - } +Keep the values from Getting Started where your app can read them: - fun checkUserIsNotLoggedIn() { - loginStatus.value = CometChatUIKit.getLoggedInUser() != null - } - - fun getLoginStatus(): LiveData = loginStatus +```kotlin AppCredentials.kt +object AppCredentials { + const val APP_ID = "YOUR_APP_ID" + const val REGION = "YOUR_REGION" + const val AUTH_KEY = "YOUR_AUTH_KEY" + const val FCM_PROVIDER_ID = "FCM-PROVIDER-ID" } ``` -Loads credentials from shared prefs, builds `UIKitSettings`, initializes CometChat UIKit (without auto socket), sets sample metadata, and exposes `loginStatus` so the splash can route to login vs home. - -**Repository (push token + call helpers)** +## 3. Initialize the SDK -```kotlin lines -object Repository { - fun registerFCMToken(listener: CometChat.CallbackListener) { /* fetch FCM token and call registerPushToken */ } - fun unregisterFCMToken(listener: CometChat.CallbackListener) { /* call unregisterPushToken */ } +Initialize in `Application.onCreate()` so the SDK is ready before any push arrives (including killed-app wakes). Build the config with `PNConfiguration.Builder(appId, region)`: - fun rejectCallWithBusyStatus( - call: Call, - callbackListener: CometChat.CallbackListener? = null - ) { /* reject with CALL_STATUS_BUSY and notify UIKit */ } +```kotlin MyApplication.kt +import android.app.Application +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.PNConfiguration - fun acceptCall( - call: Call, - callbackListener: CometChat.CallbackListener - ) { /* acceptCall and notify UIKit */ } - - fun rejectCall( - call: Call, - callbackListener: CometChat.CallbackListener - ) { /* rejectCall with CALL_STATUS_REJECTED and notify UIKit */ } -} -``` - -Thin wrappers that register/unregister FCM tokens with your Provider ID and perform server-side call actions (accept/reject/busy) so the caller sees the correct state even if your UI is backgrounded. - -**MyApplication (push/call lifecycle essentials)** - -```kotlin lines class MyApplication : Application() { override fun onCreate() { super.onCreate() - if (!CometChatUIKit.isSDKInitialized()) { - SplashViewModel().initUIKit(this) - } - - FirebaseApp.initializeApp(this) - addCallListener() - registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { currentActivity = activity } - override fun onActivityStarted(activity: Activity) { - if (activity !is SplashActivity && - CometChatUIKit.isSDKInitialized() && - isConnectedToWebSockets.compareAndSet(false, true) - ) { - CometChat.connect(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { isConnectedToWebSockets.set(true) } - override fun onError(e: CometChatException) { isConnectedToWebSockets.set(false) } - }) - } - currentActivity = activity - if (++activityReferences == 1 && !isActivityChangingConfigurations) { - isAppInForeground = true - } - } - override fun onActivityResumed(activity: Activity) { currentActivity = activity } - override fun onActivityPaused(activity: Activity) {} - override fun onActivityStopped(activity: Activity) { - if (activity !is SplashActivity) { - isActivityChangingConfigurations = activity.isChangingConfigurations - if (--activityReferences == 0 && !isActivityChangingConfigurations) { - isAppInForeground = false - if (CometChatUIKit.isSDKInitialized()) { - CometChat.disconnect(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { isConnectedToWebSockets.set(false) } - override fun onError(e: CometChatException) {} - }) - } - } - } - } - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - override fun onActivityDestroyed(activity: Activity) { if (currentActivity === activity) currentActivity = null } - }) - } + // Initialize the CometChat UI Kit / Chat SDK first (your existing setup) … - private fun addCallListener() { - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener() { - override fun onIncomingCallReceived(call: Call) { /* handle call UI or banner */ } - override fun onOutgoingCallAccepted(call: Call) {} - override fun onOutgoingCallRejected(call: Call) {} - override fun onIncomingCallCancelled(call: Call) {} - override fun onCallEndedMessageReceived(call: Call) {} - }) - } + val config = PNConfiguration.Builder(AppCredentials.APP_ID, AppCredentials.REGION) + .setNotificationSmallIcon(R.drawable.ic_notification) + .setVoIPEnabled(true) // full-screen incoming-call UI (default: true) + .setInlineReplyEnabled(true) // reply from the notification (default: true) + .build() - companion object { - var currentOpenChatId: String? = null - var currentActivity: Activity? = null - private var isAppInForeground = false - private val isConnectedToWebSockets = AtomicBoolean(false) - private var activityReferences = 0 - private var isActivityChangingConfigurations = false - private var LISTENER_ID: String = System.currentTimeMillis().toString() - private var tempCall: Call? = null - - fun getTempCall(): Call? = tempCall - fun setTempCall(call: Call?) { - tempCall = call - if (call == null && soundManager != null) { - soundManager?.pauseSilently() - } - } - - fun isAppInForeground(): Boolean = isAppInForeground - var soundManager: CometChatSoundManager? = null + CometChatPushNotifications.init(this, config) } } ``` -Initializes UIKit/Firebase, adds call listeners, manages websocket connect/disconnect tied to app foreground, tracks the current activity, and caches temp call state so banners can reappear after resume. - -State to set at runtime: - -- `isAppInForeground`/`currentActivity` inside lifecycle callbacks. -- `currentOpenChatId` when a chat screen opens; clear on exit to suppress notifications only for the active chat. -- `tempCall` via `setTempCall(...)` when an incoming call arrives; clear on dismiss/end. `getTempCall()` is read on resume to re-show the banner. +Point `` at `MyApplication` in your manifest. -## 4. Application wiring and permissions +## 4. Forward FCM payloads to the SDK -- [`AppUtils.kt`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/utils/AppUtils.kt) + your entry screen (e.g., `HomeActivity`): request notification/mic/camera/storage permissions early. -- In `HomeActivity`, keep the VoIP permission chain and phone-account enablement so call pushes can render the native UI: +Create your own `FirebaseMessagingService`. Forward each data message to `handlePushNotification(...)` and each token to `handleTokenRefresh(...)`: -```kotlin lines -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - AppUtils.requestNotificationPermission(this) - configureVoIP() - handleDeepLinking() // open chats based on NOTIFICATION_TYPE/NOTIFICATION_PAYLOAD -} - -private fun configureVoIP() { - CometChatVoIP.init(this, applicationInfo.loadLabel(packageManager).toString()) - launchVoIP() -} +```kotlin AppFCMService.kt +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.models.PushPlatform +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage -private fun launchVoIP() { - if (!CometChatVoIP.hasReadPhoneStatePermission(this)) { - CometChatVoIP.requestReadPhoneStatePermission(this, CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE) - return +class AppFCMService : FirebaseMessagingService() { + override fun onMessageReceived(message: RemoteMessage) { + CometChatPushNotifications.handlePushNotification(this, data = message.data) } - if (!CometChatVoIP.hasManageOwnCallsPermission(this)) { - CometChatVoIP.requestManageOwnCallsPermission(this, CometChatVoIPConstant.PermissionCode.MANAGE_OWN_CALLS) - return - } - if (!CometChatVoIP.hasAnswerPhoneCallsPermission(this)) { - CometChatVoIP.requestAnswerPhoneCallsPermission(this, CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS) - return - } - CometChatVoIP.hasEnabledPhoneAccountForVoIP(this, object : VoIPPermissionListener { - override fun onPermissionsGranted() { /* ready for call pushes */ } - override fun onPermissionsDenied(error: CometChatVoIPError?) { - CometChatVoIP.alertDialogForVoIP(this@HomeActivity) - } - }) -} - -override fun onRequestPermissionsResult(reqCode: Int, permissions: Array, results: IntArray) { - super.onRequestPermissionsResult(reqCode, permissions, results) - when (reqCode) { - AppUtils.PushNotificationPermissionCode -> if (granted(results)) { - CometChatVoIP.requestPhoneStatePermissions(this, CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE) - } - CometChatVoIPConstant.PermissionCode.READ_PHONE_STATE -> if (granted(results)) { - if (CometChatVoIP.hasManageOwnCallsPermission(this)) { - CometChatVoIP.requestAnswerPhoneCallsPermissions(this, CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS) - } else { - CometChatVoIP.requestManageOwnCallsPermissions(this, CometChatVoIPConstant.PermissionCode.MANAGE_OWN_CALLS) - } - } - CometChatVoIPConstant.PermissionCode.MANAGE_OWN_CALLS -> if (granted(results)) { - CometChatVoIP.requestAnswerPhoneCallsPermissions(this, CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS) - } - CometChatVoIPConstant.PermissionCode.ANSWER_PHONE_CALLS -> if (granted(results)) { - launchVoIP() - } - } -} -private fun granted(results: IntArray) = - results.isNotEmpty() && results[0] == PackageManager.PERMISSION_GRANTED - -// Deep link from notification payload to Chats -private fun handleDeepLinking() { - val type = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_TYPE) - val payload = intent.getStringExtra(AppConstants.FCMConstants.NOTIFICATION_PAYLOAD) ?: return - if (type == AppConstants.FCMConstants.NOTIFICATION_TYPE_MESSAGE) { - val fcmMessageDTO = Gson().fromJson(payload, FCMMessageDTO::class.java) - // Set currentOpenChatId to suppress notifications for the open chat - MyApplication.currentOpenChatId = if (fcmMessageDTO.receiverType == "user") { - fcmMessageDTO.sender - } else fcmMessageDTO.receiver - } -} -``` - -Requests notification + telecom permissions in sequence, initializes the VoIP phone account, and maps notification payload extras to set `currentOpenChatId` so you don’t alert for the chat currently open. - -## 5. Register the FCM token after login - -Call registration right after `CometChatUIKit.login()` succeeds: - -```kotlin lines -FirebaseMessaging.getInstance().token.addOnCompleteListener { task -> - if (task.isSuccessful) { - val token = task.result - CometChatNotifications.registerPushToken( + override fun onNewToken(token: String) { + // Re-binds a rotated token when a user is logged in; no-op otherwise. + CometChatPushNotifications.handleTokenRefresh( + PushPlatform.FCM_ANDROID, token, - PushPlatforms.FCM_ANDROID, - AppConstants.FCMConstants.PROVIDER_ID, - object : CometChat.CallbackListener() { - override fun onSuccess(uid: String?) { /* token registered */ } - override fun onError(e: CometChatException) { /* handle failure */ } - } + AppCredentials.FCM_PROVIDER_ID, ) } } ``` -Registers the current device token with CometChat under your Provider ID after login so the backend can target this user via FCM; retry on failure and rerun when the token rotates. +Register the service in your manifest: -Re-register on token refresh. Keep the provider ID aligned to the FCM provider you created for this app. - -Handle FCM refresh tokens too: - -```kotlin lines -// In FCMService -override fun onNewToken(token: String) { - super.onNewToken(token) - // Re-register with CometChat using your provider ID - CometChatNotifications.registerPushToken( - token, - PushPlatforms.FCM_ANDROID, - AppConstants.FCMConstants.PROVIDER_ID, - object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { /* token registered */ } - override fun onError(e: CometChatException) { /* handle failure */ } - } - ) -} +```xml AndroidManifest.xml + + + + + ``` -Ensures a rotated FCM token is re-bound to the logged-in user; without this, pushes will stop after Firebase refreshes the token. + +`handlePushNotification` also accepts optional `title`, `body`, `icon`, `uid`, and `guid` overrides — pass `uid`/`guid` if you want to control the notification grouping key. For the default experience, just pass `data`. + -## 6. Unregister the token on logout +## 5. Request permission and register the token -```kotlin lines -CometChatNotifications.unregisterPushToken(object : CometChat.CallbackListener() { - override fun onSuccess(s: String?) { /* success */ } - override fun onError(e: CometChatException) { /* handle error */ } -}) -// Then call CometChatUIKit.logout() -``` +Request `POST_NOTIFICATIONS` (Android 13+) early using the SDK helper, and register the token **after** login so it binds to the session: -## 7. Badge count - -CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (a string) representing the total unread messages across all conversations for the logged-in user. You can use this to set the app icon badge and enrich local notifications. - -### 7.1 Enable unread badge count on the CometChat Dashboard - -1. Go to **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences**. -2. Scroll to the bottom and enable the **Unread Badge Count** toggle. - -This ensures CometChat includes the `unreadMessageCount` field in every push payload sent to your app. - -### 7.2 Add the ShortcutBadger dependency - -Add the ShortcutBadger library to your app-level `build.gradle`: +```kotlin +import com.cometchat.pushnotification.helpers.CometChatPNHelper -```gradle lines -dependencies { - implementation 'me.leolin:ShortcutBadger:1.1.22@aar' +// In your entry activity, before/at login: +if (!CometChatPNHelper.hasNotificationPermission(this)) { + CometChatPNHelper.requestNotificationPermission(this, REQUEST_CODE_NOTIFICATIONS) } ``` -### 7.3 Expected payload format - -CometChat sends FCM data messages with this structure (relevant fields): - -```json -{ - "data": { - "unreadMessageCount": "5", - "title": "New Message", - "alert": "John: Hello!", - "conversationId": "user_abc123", - "conversationType": "user" - } +```kotlin +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.models.PushPlatform +import com.google.firebase.messaging.FirebaseMessaging + +// After CometChatUIKit.login(...) succeeds: +FirebaseMessaging.getInstance().token.addOnSuccessListener { token -> + CometChatPushNotifications.registerToken( + PushPlatform.FCM_ANDROID, + token, + AppCredentials.FCM_PROVIDER_ID, + onSuccess = { /* token registered */ }, + onError = { e -> /* log e */ }, + ) } ``` -`unreadMessageCount` is a string representing the total unread messages across all conversations for the logged-in user. - -### 7.4 Update the app badge from the push payload - -Inside your notification service (for example `FCMService.onMessageReceived`), parse `unreadMessageCount` and update the badge: +Unregister **before** logout so the device stops receiving pushes for that user: -```kotlin lines -import me.leolin.shortcutbadger.ShortcutBadger - -// Inside onMessageReceived, after receiving the message: -val unreadCountStr: String? = message.data["unreadMessageCount"] -unreadCountStr?.toIntOrNull()?.let { count -> - if (count >= 0) { - ShortcutBadger.applyCount(applicationContext, count) - } else { - Log.w(TAG, "Invalid badge count: $count") - } -} ?: Log.d(TAG, "No unreadMessageCount in payload") +```kotlin +CometChatPushNotifications.unregisterToken( + onSuccess = { /* then CometChatUIKit.logout(...) */ }, + onError = { e -> /* log e */ }, +) ``` -`ShortcutBadger` uses launcher-specific APIs (Samsung, Huawei, Xiaomi, etc.) to display a badge number on the app icon. Passing `0` clears the badge. +## 6. Notification taps and call events -### 7.5 Show unread count in the notification +Suppress notifications for the chat that is currently open, and set the tap listener so you can navigate when a chat notification is tapped: -Update your notification builder to display the unread count in the notification itself: - -```kotlin lines -// Inside your notification building logic -mNotificationBuilder.setNumber(count) -mNotificationBuilder.setSubText("$count unread messages") +```kotlin +import com.cometchat.pushnotification.CometChatPushNotifications +import com.cometchat.pushnotification.listeners.NotificationTapListener + +// When a chat screen opens (clear it on exit): +CometChatPushNotifications.setCurrentOpenChatId(conversationId) + +CometChatPushNotifications.setOnNotificationTapListener(object : NotificationTapListener { + override fun onNotificationTapped( + context: Context, + user: User?, + group: Group?, + message: BaseMessage?, + ) { + // Build your own Intent to open the conversation for `user` / `group`. + } +}) ``` -- `setNumber(count)` displays a count badge on the notification icon. -- `setSubText()` shows the unread count below the notification title. - -### 7.6 Clear badge when the app opens - -Clear the badge count when the app launches and every time it resumes from the background. Override `onResume()` in your main activity: +By default the SDK shows its **built-in full-screen incoming-call screen** and launches the CometChat Calls UI on accept — you don't need to write any call UI. Optionally, observe foreground call events to drive your own in-app UI: -```kotlin lines -override fun onResume() { - super.onResume() - ShortcutBadger.removeCount(this) -} -``` +```kotlin +import com.cometchat.pushnotification.listeners.CallEventListener +import com.cometchat.pushnotification.models.PNCallInfo -This ensures the badge is cleared when the user opens the app, keeping the badge count in sync with the actual unread state. - -{/* ## 9. What arrives in the push payload - -Payload keys delivered to `onMessageReceived` (adapted from the shared push integration): - -```json lines -{ - "title": "Andrew Joseph", - "body": "Hello!", - "sender": "cometchat-uid-1", - "senderName": "Andrew Joseph", - "senderAvatar": "https://assets.cometchat.io/sampleapp/v2/users/cometchat-uid-1.webp", - "receiver": "cometchat-uid-2", - "receiverName": "George Alan", - "receiverAvatar": "https://assets.cometchat.io/sampleapp/v2/users/cometchat-uid-2.webp", - "receiverType": "user", - "tag": "123", - "conversationId": "cometchat-uid-1_user_cometchat-uid-2", - "type": "chat", // or "call" - "callAction": "initiated", // "initiated" | "cancelled" | "unanswered" | "ongoing" | "rejected" | "ended" | "busy" - "sessionId": "v1.123.aik2", - "callType": "audio", - "sentAt": "1741847453000", - "message": { }, // CometChat Message Object if included - "custom": { } // Custom JSON if configured -} +CometChatPushNotifications.setCallEventListener(object : CallEventListener { + override fun onIncomingCall(callInfo: PNCallInfo) { /* callInfo.sessionId, callerName, callType … */ } + override fun onCallCancelled(sessionId: String) { /* tear down UI */ } +}) ``` -Use `message` for deep links (`CometChatHelper.processMessage`) and `type/callAction` to branch chat vs call flows. */} - -## 8. Handle message pushes - -- [`FCMService.onMessageReceived`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMService.kt) checks `message.data["type"]`. -- For `type == "chat"`: mark delivered (`CometChat.markAsDelivered`), skip notifying if the chat is already open (`MyApplication.currentOpenChatId`), and build grouped notifications (avatars + BigText) via [`FCMMessageNotificationUtils`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMMessageNotificationUtils.kt) with inline reply actions. -- [`FCMMessageBroadcastReceiver`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMMessageBroadcastReceiver.kt) handles inline replies, initializes the SDK headlessly, sends the reply, and refreshes the notification. -- In your messaging service (e.g., `FCMService`), set the notification tap intent to your splash/entry activity (e.g., `SplashActivity`), and keep the `currentOpenChatId` check to suppress notifications for the open chat. - -## 9. Handle call pushes (ConnectionService) + +To fully replace the built-in call screen with your own, set `setOnIncomingCallHandler(...)` (override the ringing screen) and/or `setOnCallAnsweredHandler(...)` (override the post-accept ongoing-call screen). The SDK still manages ringtone, ring timeout, and cleanup. + -- For `type == "call"`, `FCMService.handleCallFlow` parses [`FCMCallDto`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/fcm/FCMCallDto.kt) and routes to the `voip` package. -- [`CometChatVoIP`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/voip/CometChatVoIP.kt) registers a `PhoneAccount` and triggers `TelecomManager.addNewIncomingCall` for native full-screen UI with Accept/Decline. -- Busy logic: if already on a call, reject with busy (`Repository.rejectCallWithBusyStatus`). Cancel/timeout pushes end the active telecom call when IDs match. -- Runtime VoIP checks: before handling call pushes, request `READ_PHONE_STATE`, `MANAGE_OWN_CALLS`, and `ANSWER_PHONE_CALLS` at runtime and ensure the phone account is enabled (`CometChatVoIP.hasEnabledPhoneAccountForVoIP`). -- Foreground suppression: the sample ignores VoIP banners if `MyApplication.isAppInForeground()` is true; keep or remove based on your UX. -- Cancel/unanswered handling: on `callAction` of `cancelled`/`unanswered`, end the active telecom call if the session IDs match. +## 7. Badge count -## 10. Customize notification text or parse payloads +CometChat's Enhanced Push Notification payload includes an `unreadMessageCount` field (a string, total unread across all conversations). Enable it once on the dashboard: **CometChat Dashboard → Notification Engine → Settings → Preferences → Push Notification Preferences → Unread Badge Count**. -Parse the push into a `BaseMessage` for deep links: +Parse it from the payload and set the app-icon badge with a launcher badge library such as `ShortcutBadger`: ```kotlin -override fun onMessageReceived(remoteMessage: RemoteMessage) { - val messageJson = remoteMessage.data["message"] ?: return - val baseMessage = CometChatHelper.processMessage(JSONObject(messageJson)) - // open the right chat/thread using baseMessage -} +val count = message.data["unreadMessageCount"]?.toIntOrNull() ?: 0 +ShortcutBadger.applyCount(applicationContext, count) // 0 clears the badge ``` -Parses the CometChat message JSON shipped in the payload into a `BaseMessage` so you can navigate to the right conversation/thread without extra API calls. - -Override the push body before sending: - -```kotlin -val meta = JSONObject().put("pushNotification", "Custom notification body") -customMessage.metadata = meta -CometChat.sendCustomMessage(customMessage, object : CallbackListener() {}) -``` +Clear the badge when the app resumes (`onResume` of your main activity). -Adds a `pushNotification` field in metadata so CometChat uses your custom text as the push body for that message. -## 11. Navigation from notifications -Notification taps launch [`SplashActivity`](https://github.com/cometchat/cometchat-uikit-android/blob/v5/sample-app-kotlin%2Bpush-notification/src/main/java/com/cometchat/sampleapp/kotlin/fcm/ui/activity/SplashActivity.kt); it reads `NOTIFICATION_PAYLOAD` extras and opens the correct user or group in `MessagesActivity`. Keep `launchMode` settings that allow the intent extras to arrive. -## 12. Testing checklist +## 8. Testing checklist 1. Install on a physical device and grant notification + mic permissions (Android 13+ needs `POST_NOTIFICATIONS`). -2. Log in and ensure token registration succeeds (check Logcat). +2. Log in and confirm token registration succeeds (check the `registerToken` success callback / Logcat). 3. Send a message from another user: - - Foreground: grouped notification shows unless you are already in that chat. - - Background/terminated: tap opens the correct conversation. -4. Inline reply from the shade delivers the message and updates the notification. -5. Trigger an incoming call push: - - Native full-screen call UI appears with caller info. - - Accept/Decline work; cancel/timeout dismisses the telecom call. -6. Reinstall or clear app data to confirm token re-registration works. + - App open in a different chat: notification appears (grouped, with inline reply). + - App backgrounded/killed: notification appears; tapping opens the right conversation via `onNotificationTapped`. + - No notification for the chat currently open (via `setCurrentOpenChatId`). +4. Trigger an incoming CometChat call and confirm the full-screen call UI shows the caller with Accept/Decline, even on the lock screen; Accept joins the call, Decline rejects it. +5. Toggle Wi-Fi/cellular and reinstall to confirm token registration survives refreshes (`onNewToken` → `handleTokenRefresh`). -## Troubleshooting +## 9. Troubleshooting | Symptom | Quick checks | | --- | --- | -| No notifications | Package name matches Firebase app, `google-services.json` is present, notification permission granted, Provider ID correct, Push Notifications enabled. | -| Token registration fails | Run registration after login, confirm `AppConstants.FCMConstants.PROVIDER_ID`, and verify the Firebase project matches the app ID. | -| Notification tap does nothing | Ensure `SplashActivity` reads `NOTIFICATION_PAYLOAD` and activity launch modes do not drop extras. | -| Call UI never shows | All telecom permissions declared + granted; `CometChatVoIPConnectionService` in manifest; device supports `MANAGE_OWN_CALLS`. | -| Inline reply crashes | Keep `FCMMessageBroadcastReceiver` registered; do not strip FCM or `RemoteInput` classes in ProGuard/R8. | -| Badge count not showing | Verify **Unread Badge Count** is enabled in CometChat Dashboard, ShortcutBadger dependency is added, and the launcher supports badges (Samsung, Huawei, Xiaomi). | +| No notifications received | Confirm `google-services.json` is in the app module, the package name matches Firebase, `POST_NOTIFICATIONS` is granted (Android 13+), and your `FirebaseMessagingService` calls `handlePushNotification(this, data = message.data)`. | +| Token registration errors | Verify `FCM_PROVIDER_ID` matches the dashboard exactly and that `registerToken` runs **after** `login` succeeds. | +| Nothing happens on a killed-app push | Ensure `CometChatPushNotifications.init(...)` runs in `Application.onCreate()` and `` points at your `Application` subclass. | +| Full-screen call UI not showing | Test on a physical device; on aggressive OEM skins (MIUI/Redmi/POCO), grant autostart/lock-screen/overlay permissions for the app. | +| Pushes stop after some time | Make sure `onNewToken` forwards to `handleTokenRefresh` (or re-`registerToken`) — FCM rotates tokens. | + +## Resources + + + + The drop-in push & VoIP SDK on the CometChat Maven repo. + + + Source, changelog, and sample. + +